sinja 1.1.0.pre4 → 1.2.0.pre2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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