class Rack::Scaffold

Constants

ACTIONS

Public Class Methods

adapters() click to toggle source
# File lib/rack/scaffold/adapters.rb, line 3
def self.adapters
  @@adapters ||= []
end
new(options = {}) click to toggle source
# File lib/rack/scaffold.rb, line 15
def initialize(options = {})
  raise ArgumentError, "Missing option: :model or :models" unless options[:model] or options[:models]

  if options[:models] and options[:models].kind_of?(Array)
    @app = Rack::Cascade.new(options.delete(:models).collect{|model| self.class.new(options.dup.merge({model: model}))}) and return
  end

  @app = Class.new(Sinatra::Base) do
    use Rack::PostBodyContentTypeParser
    register Sinatra::MultiRoute
    helpers Sinatra::Param

    before do
      content_type :json
    end

    disable :raise_errors, :show_exceptions

    def last_modified_time(resource, resources)
      update_timestamp_field = resource.update_timestamp_field.to_sym
      most_recently_updated = resources.class.include?(Enumerable) ? resources.max_by(&update_timestamp_field) : resources

      timestamp = request.env['HTTP_IF_MODIFIED_SINCE']
      timestamp = most_recently_updated.send(update_timestamp_field) if most_recently_updated
      timestamp
    end

    def notify!(record)
      return unless @@connections
      
      pathname = Pathname.new(request.path)

      lines = []
      lines << "event: patch"

      op = case status
           when 201 then :add
           when 204 then :remove
           else
             :update
           end

      data = [{op: op, path: record.url, value: record}].to_json

      @@connections[pathname.dirname].each do |out|
        out << "event: patch\ndata: #{data}\n\n"
      end
    end
  end

  @actions = (options[:only] || ACTIONS) - (options[:except] || [])

  @adapter = Rack::Scaffold.adapters.detect{|adapter| adapter === options[:model]}
  raise "No suitable adapters found for #{options[:model]} in #{Rack::Scaffold.adapters}" unless @adapter

  resources = Array(@adapter.resources(options[:model], options))
  resources.each do |resource|
    @app.instance_eval do
      @@connections = Hash.new([])

      route :get, :subscribe, "/#{resource.plural}/?" do
        pass unless request.accept? 'text/event-stream'

        content_type 'text/event-stream'

        stream :keep_open do |out|
          @@connections[request.path] << out

          out.callback do
            @@connections[request.path].delete(out)
          end
        end
      end
    end if @actions.include?(:subscribe)

    @app.instance_eval do
      post "/#{resource.plural}/?" do
        if record = resource.create!(params)
          status 201
          notify!(record)
          {"#{resource.singular}" => record}.to_json
        else
          status 406
          {errors: record.errors}.to_json
        end
      end
    end if @actions.include?(:create)

    @app.instance_eval do
      get "/#{resource.plural}/?" do
        if params[:page] or params[:per_page]
          param :page, Integer, default: 1, min: 1
          param :per_page, Integer, default: 300, in: (1..300)

          resources = resource.paginate(params[:per_page], (params[:page] - 1) * params[:per_page])
          last_modified(last_modified_time(resource, resources)) if resource.timestamps?

          {
            "#{resource.plural}" => resources,
            page: params[:page],
            total: resource.count
          }.to_json
        else
          param :limit, Integer, default: 300, in: (1..300)
          param :offset, Integer, default: 0, min: 0

          resources = resource.paginate(params[:limit], params[:offset])
          last_modified(last_modified_time(resource, resources)) if resource.timestamps?

          {
            "#{resource.plural}" => resources
          }.to_json
        end
      end

      get "/#{resource.plural}/:id/?" do
        record = resource[params[:id]] or halt 404
        last_modified(last_modified_time(resource, record)) if resource.timestamps?
        {"#{resource.singular}" => record}.to_json
      end

      resource.one_to_many_associations.each do |association|
        get "/#{resource.plural}/:id/#{association}/?" do
          record = resource[params[:id]] or halt 404
          associations = record.send(association)

          {
            "#{association}" => associations
          }.to_json
        end
      end          
    end if @actions.include?(:read)

    @app.instance_eval do
      route :put, :patch, "/#{resource.plural}/:id/?" do
        record = resource[params[:id]] or halt 404
        if record.update!(params)
          status 200
          notify!(record)
          {"#{resource.singular}" => record}.to_json
        else
          status 406
          {errors: record.errors}.to_json
        end
      end
    end if @actions.include?(:update)

    @app.instance_eval do
      delete "/#{resource.plural}/:id/?" do
        record = resource[params[:id]] or halt 404
        if record.destroy
          status 204
          notify!(record)
        else
          status 406
          {errors: record.errors}.to_json
        end
      end
    end if @actions.include?(:destroy)
  end
end

Public Instance Methods

call(env) click to toggle source
# File lib/rack/scaffold.rb, line 177
def call(env)
  @app.call(env)
end
last_modified_time(resource, resources) click to toggle source
# File lib/rack/scaffold.rb, line 33
def last_modified_time(resource, resources)
  update_timestamp_field = resource.update_timestamp_field.to_sym
  most_recently_updated = resources.class.include?(Enumerable) ? resources.max_by(&update_timestamp_field) : resources

  timestamp = request.env['HTTP_IF_MODIFIED_SINCE']
  timestamp = most_recently_updated.send(update_timestamp_field) if most_recently_updated
  timestamp
end
notify!(record) click to toggle source
# File lib/rack/scaffold.rb, line 42
def notify!(record)
  return unless @@connections
  
  pathname = Pathname.new(request.path)

  lines = []
  lines << "event: patch"

  op = case status
       when 201 then :add
       when 204 then :remove
       else
         :update
       end

  data = [{op: op, path: record.url, value: record}].to_json

  @@connections[pathname.dirname].each do |out|
    out << "event: patch\ndata: #{data}\n\n"
  end
end