class Rack::Scaffold

Constants

ACTIONS
VERSION

Public Class Methods

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

  if options[:models]&.is_a?(Array)
    (@app = Rack::Cascade.new(options.delete(:models).collect { |model| self.class.new(options.dup.merge(model: model)) })) && 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
    set :raise_errors, true if ENV['RACK_ENV'] == 'test'

    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|
    if @actions.include?(:subscribe)
      @app.instance_eval do
        @@connections = Hash.new([])

        route :get, :subscribe, "/#{resource.plural}/?" do
          pass unless request.accept.include? '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
    end

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

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

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

            {
              resource.plural.to_s => resources,
              page: params[:page],
              total: resource.count
            }.to_json
          else
            param :limit, Integer, default: 100, in: (1..100)
            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.to_s => resources
            }.to_json
          end
        end

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

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

            {
              association.to_s => associations
            }.to_json
          end
        end
      end
    end

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

    next unless @actions.include?(:destroy)

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

Public Instance Methods

call(env) click to toggle source
# File lib/rack/scaffold.rb, line 191
def call(env)
  @app.call(env)
end
last_modified_time(resource, resources) click to toggle source
# File lib/rack/scaffold.rb, line 36
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 45
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