Skip to content

Instantly share code, notes, and snippets.

@robinedman
Last active December 26, 2015 12:39
Show Gist options
  • Save robinedman/7152602 to your computer and use it in GitHub Desktop.
Save robinedman/7152602 to your computer and use it in GitHub Desktop.
Way of exposing Cuba models to a client over a REST API.
#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
#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
# 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