Skip to content

Instantly share code, notes, and snippets.

@jgaskins
Last active August 15, 2020 02:34
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save jgaskins/6484e8037a9f0896aff9b54e7fb736a5 to your computer and use it in GitHub Desktop.
Save jgaskins/6484e8037a9f0896aff9b54e7fb736a5 to your computer and use it in GitHub Desktop.
API app with Roda
require 'authentication'
class MyApp < Roda
include Authentication
plugin :json
route do |r|
r.on('docs') { r.run Docs }
authenticate! ### EVERYTHING BELOW THIS LINE IS AUTHENTICATED ###
r.on('users') { r.run Users }
r.on('groups') { r.run Groups }
# etc
end
def authenticate!
unauthenticated! unless current_user
end
def unauthenticated!
request.halt :unauthenticated, error: 'Must be authenticated'
end
end
# This mixin depends on the including class defining an `env` method
# that returns a Rack env hash.
module Authentication
def current_user
# We memoize things onto the `env` hash instead of on instances
# because of how it's based on the request rather than the class
# of the application. We need it to work when passing the request
# between app classes.
env[:current_user] ||= AuthToken
.find_by(id: auth_token)
&.user
end
def auth_token
env[:auth_token] ||= env['HTTP_AUTHORIZATION']
.to_s
.sub(/\ABearer /, '')
end
end
require 'primalize'
class Route < Roda
# Automatically JSONify hashes and serializers. I prefer the primalize
# gem for JSON serializers, so we explicitly mention that the response
# serializers should be included for automatic JSON conversion, as well.
plugin :json, classes: [Hash, Primalize::Many]
# If we receive a JSON request body, parse it
plugin :json_parser, parser: -> str { JSON.parse str, symbolize_names: true }
# By default, Roda only provides support for GET and POST because
# they're the only ones that HTML forms support
plugin :all_verbs
# This plugin lets you do things like:
# response.status = :forbidden
# instead of:
# response.status = 403
plugin :symbol_status
end
# Inherit from our application route superclass
class Users < Route
route do |r|
# This is the root of *this* handler after being handed off from a parent route or the main app
r.root do
users = User.includes(:groups)
UsersResponse.new(users: users, groups: users.map(&:groups).uniq)
end
# Kinda weird that this is how Roda wants you to post to the root path, but it's fine
r.post '' do
user = User.new(r.params[:user])
if user.save
UserResponse.new(user: user, groups: user.groups)
else
response.status = :unprocessable_entity
{ errors: user.errors }
end
end
r.on String do |user_id|
user = User.find(user_id)
r.get do
UserResponse.new(user: user, groups: user.groups)
end
r.put do
user.update! r.params[:user]
UserResponse.new(user: user, groups: user.groups)
end
r.delete do
user.destroy
{}
end
end
end
class UserSerializer < Primalize::Single
attributes(
id: string,
name: string,
email: string,
group_ids: array(string),
)
end
class UsersResponse < Primalize::Many
attributes(
users: enumerable(UserSerializer),
groups: enumerable(GroupSerializer),
)
end
class UserResponse < Primalize::Many
attributes(
user: UserSerializer,
groups: enumerable(GroupSerializer),
)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment