Class: Rack::Scaffold

Inherits:
Object
  • Object
show all
Defined in:
lib/rack/scaffold.rb,
lib/rack/scaffold/version.rb,
lib/rack/scaffold/adapters.rb

Defined Under Namespace

Modules: Adapters

Constant Summary collapse

ACTIONS =
%i[subscribe create read update destroy].freeze
VERSION =
'0.2.2'

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Scaffold

Returns a new instance of Scaffold.

Raises:

  • (ArgumentError)


17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# 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

Class Method Details

.adaptersObject



5
6
7
# File 'lib/rack/scaffold/adapters.rb', line 5

def self.adapters
  @@adapters ||= []
end

Instance Method Details

#call(env) ⇒ Object



191
192
193
# File 'lib/rack/scaffold.rb', line 191

def call(env)
  @app.call(env)
end