sinja 1.1.0.pre4 → 1.2.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -1
  3. data/README.md +96 -29
  4. data/demo-app/.dockerignore +1 -0
  5. data/demo-app/Dockerfile +26 -0
  6. data/demo-app/Gemfile +12 -1
  7. data/demo-app/README.md +39 -24
  8. data/demo-app/Rakefile +36 -0
  9. data/demo-app/app.rb +3 -10
  10. data/demo-app/boot.rb +0 -4
  11. data/demo-app/classes/author.rb +3 -4
  12. data/demo-app/{base.rb → classes/base.rb} +2 -1
  13. data/demo-app/classes/comment.rb +1 -2
  14. data/demo-app/classes/post.rb +17 -10
  15. data/demo-app/classes/tag.rb +7 -3
  16. data/extensions/sequel/Gemfile +4 -0
  17. data/extensions/sequel/LICENSE.txt +21 -0
  18. data/extensions/sequel/README.md +274 -0
  19. data/extensions/sequel/Rakefile +10 -0
  20. data/extensions/sequel/bin/console +14 -0
  21. data/extensions/sequel/bin/setup +8 -0
  22. data/extensions/sequel/lib/sinatra/jsonapi/sequel.rb +7 -0
  23. data/extensions/sequel/lib/sinja-sequel.rb +2 -0
  24. data/extensions/sequel/lib/sinja/sequel.rb +110 -0
  25. data/extensions/sequel/lib/sinja/sequel/core.rb +72 -0
  26. data/extensions/sequel/lib/sinja/sequel/helpers.rb +78 -0
  27. data/extensions/sequel/lib/sinja/sequel/version.rb +6 -0
  28. data/extensions/sequel/sinja-sequel.gemspec +29 -0
  29. data/extensions/sequel/test/test_helper.rb +3 -0
  30. data/lib/sinja.rb +40 -18
  31. data/lib/sinja/config.rb +4 -3
  32. data/lib/sinja/helpers/serializers.rb +2 -1
  33. data/lib/sinja/relationship_routes/has_many.rb +8 -9
  34. data/lib/sinja/relationship_routes/has_one.rb +1 -1
  35. data/lib/sinja/resource.rb +2 -1
  36. data/lib/sinja/resource_routes.rb +2 -2
  37. data/lib/sinja/version.rb +1 -1
  38. data/sinja.gemspec +5 -7
  39. metadata +39 -11
  40. data/.rspec +0 -2
  41. data/lib/sinja/extensions/sequel.rb +0 -53
  42. data/lib/sinja/helpers/sequel.rb +0 -101
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'sinja-sequel'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require 'pry'
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ require 'sinja/sequel'
3
+ require 'sinatra/jsonapi'
4
+
5
+ module Sinatra
6
+ register JSONAPI::Sequel
7
+ end
@@ -0,0 +1,2 @@
1
+ # frozen_string_literal: true
2
+ require 'sinja/sequel'
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+ require 'sinja/sequel/helpers'
3
+ require 'sinja/sequel/version'
4
+
5
+ module Sinja
6
+ module Sequel
7
+ def self.registered(app)
8
+ app.helpers Helpers
9
+ end
10
+
11
+ def resource(res, try_convert=:to_i, &block)
12
+ klass = res.to_s.classify.constantize
13
+
14
+ super(res) do
15
+ register Resource
16
+
17
+ helpers do
18
+ def find(id)
19
+ klass[id.send(try_convert)]
20
+ end
21
+ end
22
+
23
+ show
24
+
25
+ show_many do |ids|
26
+ klass.where(klass.primary_key=>ids.map(&try_convert)).all
27
+ end
28
+
29
+ index do
30
+ klass.dataset
31
+ end
32
+
33
+ create do |attr|
34
+ tmp = klass.new
35
+ if respond_to?(:settable_fields)
36
+ tmp.set_fields(attr, settable_fields)
37
+ else
38
+ tmp.set(attr)
39
+ end
40
+ tmp.save(:validate=>false)
41
+ next_pk tmp
42
+ end
43
+
44
+ update do |attr|
45
+ if respond_to?(:settable_fields)
46
+ resource.update_fields(attr, settable_fields, :validate=>false, :missing=>:skip)
47
+ else
48
+ resource.set(attr)
49
+ resource.save_changes(:validate=>false)
50
+ end
51
+ end
52
+
53
+ destroy do
54
+ resource.destroy
55
+ end
56
+
57
+ instance_eval(&block) if block
58
+ end
59
+ end
60
+
61
+ module Resource
62
+ def has_one(rel, &block)
63
+ super(rel) do
64
+ pluck do
65
+ resource.send(rel)
66
+ end
67
+
68
+ prune(:sideload_on=>:update) do
69
+ resource.send("#{rel}=", nil)
70
+ resource.save_changes
71
+ end
72
+
73
+ graft(:sideload_on=>%i[create update]) do |rio|
74
+ klass = resource.class.association_reflection(rel).associated_class
75
+ resource.send("#{rel}=", klass.with_pk!(rio[:id]))
76
+ resource.save_changes(:validate=>!sideloaded?)
77
+ end
78
+
79
+ instance_eval(&block) if block
80
+ end
81
+ end
82
+
83
+ def has_many(rel, &block)
84
+ super(rel) do
85
+ fetch do
86
+ resource.send("#{rel}_dataset")
87
+ end
88
+
89
+ clear(:sideload_on=>:update) do
90
+ resource.send("remove_all_#{rel}")
91
+ end
92
+
93
+ replace(:sideload_on=>:update) do |rios|
94
+ add_remove(rel, rios)
95
+ end
96
+
97
+ merge(:sideload_on=>:create) do |rios|
98
+ add_missing(rel, rios)
99
+ end
100
+
101
+ subtract do |rios|
102
+ remove_present(rel, rios)
103
+ end
104
+
105
+ instance_eval(&block) if block
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+ require 'forwardable'
3
+ require 'sequel'
4
+
5
+ module Sinja
6
+ module Sequel
7
+ module Core
8
+ extend Forwardable
9
+
10
+ def self.prepended(base)
11
+ base.sinja do |c|
12
+ c.conflict_exceptions << ::Sequel::ConstraintViolation
13
+ c.not_found_exceptions << ::Sequel::NoMatchingRow
14
+ c.validation_exceptions << ::Sequel::ValidationFailed
15
+ c.validation_formatter = ->(e) { e.errors.keys.zip(e.errors.full_messages) }
16
+ end
17
+
18
+ base.include Pagination if ::Sequel::Database::EXTENSIONS.key?(:pagination)
19
+ end
20
+
21
+ def_delegator ::Sequel::Model, :db, :database
22
+
23
+ def_delegator :database, :transaction
24
+
25
+ define_method :filter, proc(&:where)
26
+
27
+ def sort(collection, fields)
28
+ collection.order(*fields.map { |k, v| ::Sequel.send(v, k) })
29
+ end
30
+
31
+ define_method :finalize, proc(&:all)
32
+
33
+ def validate!
34
+ raise ::Sequel::ValidationFailed, resource unless resource.valid?
35
+ end
36
+ end
37
+
38
+ module Pagination
39
+ def self.included(base)
40
+ base.sinja { |c| c.page_using = {
41
+ :number=>1,
42
+ :size=>10,
43
+ :record_count=>nil
44
+ }}
45
+ end
46
+
47
+ def page(collection, opts)
48
+ collection = collection.dataset unless collection.respond_to?(:paginate)
49
+
50
+ opts = settings._sinja.page_using.merge(opts)
51
+ collection = collection.paginate opts[:number].to_i, opts[:size].to_i,
52
+ (opts[:record_count].to_i if opts[:record_count])
53
+
54
+ # Attributes common to all pagination links
55
+ base = {
56
+ :size=>collection.page_size,
57
+ :record_count=>collection.pagination_record_count
58
+ }
59
+
60
+ pagination = {
61
+ :first=>base.merge(:number=>1),
62
+ :self=>base.merge(:number=>collection.current_page),
63
+ :last=>base.merge(:number=>collection.page_count)
64
+ }
65
+ pagination[:next] = base.merge(:number=>collection.next_page) if collection.next_page
66
+ pagination[:prev] = base.merge(:number=>collection.prev_page) if collection.prev_page
67
+
68
+ return collection, pagination
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+ require 'sinja/sequel/core'
3
+
4
+ module Sinja
5
+ module Sequel
6
+ module Helpers
7
+ def self.included(base)
8
+ base.prepend Core
9
+ end
10
+
11
+ def next_pk(resource, opts={})
12
+ [resource.pk, resource, opts]
13
+ end
14
+
15
+ def add_remove(association, rios, try_convert=:to_i)
16
+ meth_suffix = association.to_s.singularize
17
+ add_meth = "add_#{meth_suffix}".to_sym
18
+ remove_meth = "remove_#{meth_suffix}".to_sym
19
+
20
+ dataset = resource.send("#{association}_dataset")
21
+ klass = dataset.association_reflection.associated_class
22
+
23
+ # does not / will not work with composite primary keys
24
+ new_ids = rios.map { |rio| rio[:id].send(try_convert) }
25
+ transaction do
26
+ resource.lock!
27
+ old_ids = dataset.select_map(klass.primary_key)
28
+ in_common = old_ids & new_ids
29
+
30
+ (new_ids - in_common).each do |id|
31
+ subresource = klass.with_pk!(id)
32
+ resource.send(add_meth, subresource) \
33
+ unless block_given? && !yield(subresource)
34
+ end
35
+
36
+ (old_ids - in_common).each do |id|
37
+ subresource = klass.with_pk!(id)
38
+ resource.send(remove_meth, subresource) \
39
+ unless block_given? && !yield(subresource)
40
+ end
41
+
42
+ resource.reload
43
+ end
44
+ end
45
+
46
+ def add_missing(*args, &block)
47
+ add_or_remove(:add, :-, *args, &block)
48
+ end
49
+
50
+ def remove_present(*args, &block)
51
+ add_or_remove(:remove, :&, *args, &block)
52
+ end
53
+
54
+ private
55
+
56
+ def add_or_remove(meth_prefix, operator, association, rios, try_convert=:to_i)
57
+ meth = "#{meth_prefix}_#{association.to_s.singularize}".to_sym
58
+ transaction do
59
+ resource.lock!
60
+ venn(operator, association, rios, try_convert) do |subresource|
61
+ resource.send(meth, subresource) \
62
+ unless block_given? && !yield(subresource)
63
+ end
64
+ resource.reload
65
+ end
66
+ end
67
+
68
+ def venn(operator, association, rios, try_convert)
69
+ dataset = resource.send("#{association}_dataset")
70
+ klass = dataset.association_reflection.associated_class
71
+ # does not / will not work with composite primary keys
72
+ rios.map { |rio| rio[:id].send(try_convert) }
73
+ .send(operator, dataset.select_map(klass.primary_key))
74
+ .each { |id| yield klass.with_pk!(id) }
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ module Sinja
3
+ module Sequel
4
+ VERSION = '0.1.0'
5
+ end
6
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sinja/sequel/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'sinja-sequel'
8
+ spec.version = Sinja::Sequel::VERSION
9
+ spec.authors = ['Mike Pastore']
10
+ spec.email = ['mike@oobak.org']
11
+
12
+ spec.summary = 'Sequel-specific Helpers and DSL for Sinja'
13
+ spec.homepage = 'https://github.com/mwpastore/sinja/tree/master/extensions/sequel'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.require_paths = %w[lib]
20
+
21
+ spec.required_ruby_version = '>= 2.3.0'
22
+
23
+ spec.add_dependency 'sequel', '~> 4.0'
24
+ spec.add_dependency 'sinja', '>= 1.2.0.pre2', '< 2'
25
+
26
+ spec.add_development_dependency 'bundler', '~> 1.11'
27
+ spec.add_development_dependency 'minitest', '~> 5.9'
28
+ spec.add_development_dependency 'rake', '~> 12.0'
29
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+ require 'bundler/setup'
3
+ require 'minitest/autorun'
data/lib/sinja.rb CHANGED
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
+ require 'set'
3
+
2
4
  require 'active_support/inflector'
3
5
  require 'mustermann'
4
6
  require 'sinatra/base'
5
7
  require 'sinatra/namespace'
6
8
 
7
- require 'set'
8
9
  require 'sinja/config'
9
10
  require 'sinja/errors'
10
11
  require 'sinja/helpers/serializers'
@@ -220,8 +221,11 @@ module Sinja
220
221
  end
221
222
 
222
223
  def filter_by?(action)
223
- return true if settings.resource_config[action][:filter_by].empty? ||
224
- params[:filter].keys.to_set.subset?(settings.resource_config[action][:filter_by])
224
+ return if params[:filter].empty?
225
+
226
+ return params[:filter] \
227
+ if settings.resource_config[action][:filter_by].empty? ||
228
+ params[:filter].keys.to_set.subset?(settings.resource_config[action][:filter_by])
225
229
 
226
230
  raise BadRequestError, "Invalid `filter' query parameter(s)"
227
231
  end
@@ -239,8 +243,11 @@ module Sinja
239
243
  end
240
244
 
241
245
  def sort_by?(action)
242
- return true if settings.resource_config[action][:sort_by].empty? ||
243
- params[:sort].keys.to_set.subset?(settings.resource_config[action][:sort_by])
246
+ return if params[:sort].empty?
247
+
248
+ return params[:sort] \
249
+ if settings.resource_config[action][:sort_by].empty? ||
250
+ params[:sort].keys.to_set.subset?(settings.resource_config[action][:sort_by])
244
251
 
245
252
  raise BadRequestError, "Invalid `sort' query parameter(s)"
246
253
  end
@@ -257,25 +264,36 @@ module Sinja
257
264
  end
258
265
 
259
266
  def page_using?
260
- return true if params[:page].keys.to_set.subset?(settings._sinja.page_using.keys.to_set)
267
+ return if params[:page].empty?
268
+
269
+ return params[:page] \
270
+ if params[:page].keys.to_set.subset?(settings._sinja.page_using.keys.to_set)
261
271
 
262
272
  raise BadRequestError, "Invalid `page' query parameter(s)"
263
273
  end
264
274
 
265
275
  def filter_sort_page?(action)
266
- filter_by?(action) unless params[:filter].empty?
267
- sort_by?(action) unless params[:sort].empty?
268
- page_using? unless params[:page].empty?
269
- end
276
+ return enum_for(__callee__, action).to_h unless block_given?
277
+
278
+ if filter = filter_by?(action)
279
+ yield :filter, filter
280
+ end
281
+
282
+ if sort = sort_by?(action)
283
+ yield :sort, sort
284
+ end
270
285
 
271
- def filter_sort_page(collection)
272
- collection = filter(collection, params[:filter]) unless params[:filter].empty?
273
- collection = sort(collection, params[:sort]) unless params[:sort].empty?
274
- collection, pagination = page(collection, params[:page]) unless params[:page].empty?
286
+ if page = page_using?
287
+ yield :page, page
288
+ end
289
+ end
275
290
 
276
- collection = finalize(collection) if respond_to?(:finalize)
291
+ def filter_sort_page(collection, opts)
292
+ collection = filter(collection, opts[:filter]) if opts.key?(:filter)
293
+ collection = sort(collection, opts[:sort]) if opts.key?(:sort)
294
+ collection, pagination = page(collection, opts[:page]) if opts.key?(:page)
277
295
 
278
- return collection, pagination
296
+ return respond_to?(:finalize) ? finalize(collection) : collection, pagination
279
297
  end
280
298
 
281
299
  def halt(code, body=nil)
@@ -306,10 +324,10 @@ module Sinja
306
324
 
307
325
  def sanity_check!(resource_name, id=nil)
308
326
  raise ConflictError, 'Resource type in payload does not match endpoint' \
309
- if data[:type].to_sym != resource_name
327
+ unless data[:type] && data[:type].to_sym == resource_name
310
328
 
311
329
  raise ConflictError, 'Resource ID in payload does not match endpoint' \
312
- if id && data[:id].to_s != id.to_s
330
+ unless id.nil? || data[:id] && data[:id].to_s == id.to_s
313
331
  end
314
332
 
315
333
  def transaction
@@ -332,6 +350,10 @@ module Sinja
332
350
  body serialize_response_body if response.ok? || response.created?
333
351
  end
334
352
 
353
+ app.not_found do
354
+ serialize_errors(&settings._sinja.error_logger)
355
+ end
356
+
335
357
  app.error 400...600 do
336
358
  serialize_errors(&settings._sinja.error_logger)
337
359
  end