Last active
December 26, 2015 12:39
-
-
Save robinedman/7152602 to your computer and use it in GitHub Desktop.
Way of exposing Cuba models to a client over a REST API.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#encoding: utf-8 | |
require 'cuba' | |
require 'mongoid' | |
require_relative 'mongoidimportexport' | |
require_relative 'modelexample' | |
def send_json(document) | |
res['Content-Type'] = 'application/json; charset=utf-8' | |
res.write ActiveSupport::JSON.encode(document) | |
end | |
Cuba.define do | |
on 'models' do | |
user = current_user(req) | |
if user == nil | |
res.status = 401 | |
else | |
# =============== | |
# REST overrides | |
# =============== | |
# ======================= | |
# Default REST interface | |
# ======================= | |
on ':model_pluralized' do |model_pluralized| | |
model = model_pluralized.singularize.camelize.constantize | |
if model.rest? | |
on ":id" do |document_id| | |
# REST read individual document | |
on get do | |
if model.rest?(:read) | |
send_json(model.find(document_id).as_external_document) | |
else | |
res.status = 401 | |
end | |
end | |
# REST update | |
on put, param('data') do |client_model| | |
if model.rest?(:update) | |
puts "REST update. #{user.email} updates document #{document_id} from #{model.name}." | |
model.find(document_id).external_update!(client_model) | |
else | |
res.status = 401 | |
end | |
end | |
# REST delete | |
on delete do | |
if model.rest?(:delete) | |
puts "REST delete. #{user.email} deletes document #{document_id} from #{model.name}." | |
model.find(document_id).delete | |
else | |
res.status = 401 | |
end | |
end | |
end | |
on get do | |
# REST read | |
if model.rest?(:read) | |
send_json(model.all.map {|m| m.as_external_document}) | |
else | |
res.status = 401 | |
end | |
end | |
# REST create | |
on post, param('data') do | |
if model.rest?(:create) | |
# TODO: keep security in mind | |
raise NotImplementedError | |
else | |
res.status = 401 | |
end | |
end | |
else | |
res.status = 401 | |
end | |
end | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#encoding: utf-8 | |
class User | |
include Mongoid::Document | |
include MongoidImportExport | |
externally_accessible :email, | |
:first_name, | |
:last_name | |
externally_readable :active | |
rest_interface :read, | |
:update, | |
:delete | |
field :email, type: String | |
field :first_name, type: String, default: "" | |
field :last_name, type: String, default: "" | |
field :active, type: Boolean, default: true | |
validates :email, presence: true, uniqueness: true, length: { maximum: 64 } | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# encoding: utf-8 | |
# While there is support for mass assignment in ActiveModel and thus | |
# in Mongoid, it seems to lack the ability to specify publicly accessible | |
# fields and fields that are only supposed to be publicly readable. | |
# Ideally we'd like to have some fields that a client should be able to | |
# update and some they should be able to read but not update. | |
# So this is a very basic import/export implemenation | |
# meant to be mixed into a Mongoid model. | |
# Also includes a basic REST security implementation. | |
# Usage: | |
# Export with as_external_document. | |
# Update attributes with hash from client with external_update. | |
module MongoidImportExport | |
def self.included(base) | |
base.const_set(:EXTERNALLY_READABLE_FIELDS, ['_id']) | |
base.const_set(:EXTERNALLY_ACCESSIBLE_FIELDS, []) | |
base.const_set(:REST_INTERFACE, []) | |
base.send(:include, InstanceMethods) | |
base.extend(ClassMethods) | |
end | |
module InstanceMethods | |
def as_external_document | |
allowed_fields = (self.class.const_get(:EXTERNALLY_ACCESSIBLE_FIELDS) + self.class.const_get(:EXTERNALLY_READABLE_FIELDS)).map(&:to_s) | |
doc = self.as_document | |
# note: id fix for client side libraries like Spine.js, | |
# who rely on an id attribute being present. | |
doc['id'] = doc['_id'] | |
doc.slice(*allowed_fields + ['id']) | |
end | |
def external_update!(document_as_hash) | |
allowed_updates = document_as_hash.slice(*self.class.const_get(:EXTERNALLY_ACCESSIBLE_FIELDS).map(&:to_s)) | |
update_attributes!(allowed_updates) | |
end | |
# Does the model allow a certain REST operation? | |
# If no operation given: Does the model allow any REST operation at all? | |
def rest?(operation) | |
if operation | |
self.class.const_get(:REST_INTERFACE).include?(operation) | |
else | |
! self.class.const_get(:REST_INTERFACE).empty? | |
end | |
end | |
end | |
module ClassMethods | |
# Externally accessible fields and embedded documents. | |
def externally_accessible(*fields) | |
const_get(:EXTERNALLY_ACCESSIBLE_FIELDS).push(*fields) | |
end | |
# Externally readable fields and embedded documents. | |
def externally_readable(*fields) | |
const_get(:EXTERNALLY_READABLE_FIELDS).push(*fields) | |
end | |
# Used to define allowed REST operations (e.g. :read, :create, :update, :delete). | |
# Example usage: | |
# class MyModel | |
# include Mongoid::Document | |
# include LingonberryMongoidImportExport | |
# | |
# rest_interface :read, :update, :delete | |
# | |
def rest_interface(*operations) | |
const_get(:REST_INTERFACE).push(*operations) | |
end | |
# Does the model allow a certain REST operation? | |
# If no operation given: Does the model allow any REST operation at all? | |
def rest?(operation = nil) | |
if operation | |
const_get(:REST_INTERFACE).include?(operation) | |
else | |
! const_get(:REST_INTERFACE).empty? | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment