jsonapi_compliable 0.11.34 → 1.0.alpha.2

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 (73) hide show
  1. checksums.yaml +5 -5
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +1 -2
  4. data/Rakefile +7 -3
  5. data/jsonapi_compliable.gemspec +7 -3
  6. data/lib/generators/jsonapi/resource_generator.rb +8 -79
  7. data/lib/generators/jsonapi/templates/application_resource.rb.erb +2 -1
  8. data/lib/generators/jsonapi/templates/controller.rb.erb +19 -64
  9. data/lib/generators/jsonapi/templates/resource.rb.erb +5 -47
  10. data/lib/generators/jsonapi/templates/resource_reads_spec.rb.erb +62 -0
  11. data/lib/generators/jsonapi/templates/resource_writes_spec.rb.erb +63 -0
  12. data/lib/jsonapi_compliable.rb +87 -18
  13. data/lib/jsonapi_compliable/adapters/abstract.rb +202 -45
  14. data/lib/jsonapi_compliable/adapters/active_record.rb +6 -130
  15. data/lib/jsonapi_compliable/adapters/active_record/base.rb +247 -0
  16. data/lib/jsonapi_compliable/adapters/active_record/belongs_to_sideload.rb +17 -0
  17. data/lib/jsonapi_compliable/adapters/active_record/has_many_sideload.rb +17 -0
  18. data/lib/jsonapi_compliable/adapters/active_record/has_one_sideload.rb +17 -0
  19. data/lib/jsonapi_compliable/adapters/active_record/inferrence.rb +12 -0
  20. data/lib/jsonapi_compliable/adapters/active_record/many_to_many_sideload.rb +30 -0
  21. data/lib/jsonapi_compliable/adapters/null.rb +177 -6
  22. data/lib/jsonapi_compliable/base.rb +33 -320
  23. data/lib/jsonapi_compliable/context.rb +16 -0
  24. data/lib/jsonapi_compliable/deserializer.rb +14 -39
  25. data/lib/jsonapi_compliable/errors.rb +227 -24
  26. data/lib/jsonapi_compliable/extensions/extra_attribute.rb +3 -1
  27. data/lib/jsonapi_compliable/filter_operators.rb +25 -0
  28. data/lib/jsonapi_compliable/hash_renderer.rb +57 -0
  29. data/lib/jsonapi_compliable/query.rb +190 -202
  30. data/lib/jsonapi_compliable/rails.rb +12 -6
  31. data/lib/jsonapi_compliable/railtie.rb +64 -0
  32. data/lib/jsonapi_compliable/renderer.rb +60 -0
  33. data/lib/jsonapi_compliable/resource.rb +35 -663
  34. data/lib/jsonapi_compliable/resource/configuration.rb +239 -0
  35. data/lib/jsonapi_compliable/resource/dsl.rb +138 -0
  36. data/lib/jsonapi_compliable/resource/interface.rb +32 -0
  37. data/lib/jsonapi_compliable/resource/polymorphism.rb +68 -0
  38. data/lib/jsonapi_compliable/resource/sideloading.rb +102 -0
  39. data/lib/jsonapi_compliable/resource_proxy.rb +127 -0
  40. data/lib/jsonapi_compliable/responders.rb +19 -0
  41. data/lib/jsonapi_compliable/runner.rb +25 -0
  42. data/lib/jsonapi_compliable/scope.rb +37 -79
  43. data/lib/jsonapi_compliable/scoping/extra_attributes.rb +29 -0
  44. data/lib/jsonapi_compliable/scoping/filter.rb +39 -58
  45. data/lib/jsonapi_compliable/scoping/filterable.rb +9 -14
  46. data/lib/jsonapi_compliable/scoping/paginate.rb +9 -3
  47. data/lib/jsonapi_compliable/scoping/sort.rb +16 -4
  48. data/lib/jsonapi_compliable/sideload.rb +221 -347
  49. data/lib/jsonapi_compliable/sideload/belongs_to.rb +34 -0
  50. data/lib/jsonapi_compliable/sideload/has_many.rb +16 -0
  51. data/lib/jsonapi_compliable/sideload/has_one.rb +9 -0
  52. data/lib/jsonapi_compliable/sideload/many_to_many.rb +24 -0
  53. data/lib/jsonapi_compliable/sideload/polymorphic_belongs_to.rb +108 -0
  54. data/lib/jsonapi_compliable/stats/payload.rb +4 -8
  55. data/lib/jsonapi_compliable/types.rb +172 -0
  56. data/lib/jsonapi_compliable/util/attribute_check.rb +88 -0
  57. data/lib/jsonapi_compliable/util/persistence.rb +29 -7
  58. data/lib/jsonapi_compliable/util/relationship_payload.rb +4 -4
  59. data/lib/jsonapi_compliable/util/render_options.rb +4 -32
  60. data/lib/jsonapi_compliable/util/serializer_attributes.rb +98 -0
  61. data/lib/jsonapi_compliable/util/validation_response.rb +15 -9
  62. data/lib/jsonapi_compliable/version.rb +1 -1
  63. metadata +105 -24
  64. data/lib/generators/jsonapi/field_generator.rb +0 -0
  65. data/lib/generators/jsonapi/templates/create_request_spec.rb.erb +0 -29
  66. data/lib/generators/jsonapi/templates/destroy_request_spec.rb.erb +0 -20
  67. data/lib/generators/jsonapi/templates/index_request_spec.rb.erb +0 -22
  68. data/lib/generators/jsonapi/templates/payload.rb.erb +0 -39
  69. data/lib/generators/jsonapi/templates/serializer.rb.erb +0 -25
  70. data/lib/generators/jsonapi/templates/show_request_spec.rb.erb +0 -19
  71. data/lib/generators/jsonapi/templates/update_request_spec.rb.erb +0 -33
  72. data/lib/jsonapi_compliable/adapters/active_record_sideloading.rb +0 -152
  73. data/lib/jsonapi_compliable/scoping/extra_fields.rb +0 -58
@@ -0,0 +1,102 @@
1
+ module JsonapiCompliable
2
+ class Resource
3
+ module Sideloading
4
+ def self.included(klass)
5
+ klass.extend ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ def allow_sideload(name, opts = {}, &blk)
10
+ klass = Class.new(opts.delete(:class) || Sideload)
11
+ klass.class_eval(&blk) if blk
12
+ opts[:parent_resource] = self
13
+ relationship_option(opts, :readable)
14
+ relationship_option(opts, :writable)
15
+ sideload = klass.new(name, opts)
16
+ if parent = opts[:parent]
17
+ parent.children[name] = sideload
18
+ else
19
+ config[:sideloads][name] = sideload
20
+ apply_sideloads_to_serializer
21
+ end
22
+ sideload
23
+ end
24
+
25
+ def apply_sideloads_to_serializer
26
+ config[:sideloads].each_pair do |name, sideload|
27
+ if serializer.relationship_blocks[name].nil? && sideload.readable?
28
+ serializer.relationship(name)
29
+ end
30
+ end
31
+ end
32
+
33
+ def has_many(name, opts = {}, &blk)
34
+ opts[:class] = adapter.sideloading_classes[:has_many]
35
+ allow_sideload(name, opts, &blk)
36
+ end
37
+
38
+ def belongs_to(name, opts = {}, &blk)
39
+ opts[:class] = adapter.sideloading_classes[:belongs_to]
40
+ allow_sideload(name, opts, &blk)
41
+ end
42
+
43
+ def has_one(name, opts = {}, &blk)
44
+ opts[:class] = adapter.sideloading_classes[:has_one]
45
+ allow_sideload(name, opts, &blk)
46
+ end
47
+
48
+ def many_to_many(name, opts = {}, &blk)
49
+ opts[:class] = adapter.sideloading_classes[:many_to_many]
50
+ allow_sideload(name, opts, &blk)
51
+ end
52
+
53
+ def polymorphic_belongs_to(name, opts = {}, &blk)
54
+ opts[:resource] ||= Class.new(::JsonapiCompliable::Resource) do
55
+ self.polymorphic = []
56
+ self.abstract_class = true
57
+ end
58
+ # adapters *probably* don't need to override this, but it's allowed
59
+ opts[:class] ||= adapter.sideloading_classes[:polymorphic_belongs_to]
60
+ opts[:class] ||= ::JsonapiCompliable::Sideload::PolymorphicBelongsTo
61
+ allow_sideload(name, opts, &blk)
62
+ end
63
+
64
+ def sideload(name)
65
+ sideloads[name]
66
+ end
67
+
68
+ def all_sideloads(memo = {})
69
+ sideloads.each_pair do |name, sideload|
70
+ unless memo[name]
71
+ memo[name] = sideload
72
+ memo.merge!(sideload.resource.class.all_sideloads(memo))
73
+ end
74
+ end
75
+ memo
76
+ end
77
+
78
+ def association_names(memo = [])
79
+ all_sideloads.each_pair do |name, sl|
80
+ unless memo.include?(sl.name)
81
+ memo << sl.name
82
+ memo |= sl.resource.class.association_names(memo)
83
+ end
84
+ end
85
+
86
+ memo
87
+ end
88
+
89
+ def association_types(memo = [])
90
+ all_sideloads.each_pair do |name, sl|
91
+ unless memo.include?(sl.resource.type)
92
+ memo << sl.resource.type
93
+ memo |= sl.resource.class.association_types(memo)
94
+ end
95
+ end
96
+
97
+ memo
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,127 @@
1
+ module JsonapiCompliable
2
+ class ResourceProxy
3
+ include Enumerable
4
+
5
+ attr_reader :resource, :query, :scope
6
+
7
+ def initialize(resource, scope, query, payload: nil, single: false, raise_on_missing: false)
8
+ @resource = resource
9
+ @scope = scope
10
+ @query = query
11
+ @payload = payload
12
+ @single = single
13
+ @raise_on_missing = raise_on_missing
14
+ end
15
+
16
+ def single?
17
+ !!@single
18
+ end
19
+
20
+ def raise_on_missing?
21
+ !!@raise_on_missing
22
+ end
23
+
24
+ def errors
25
+ data.errors
26
+ end
27
+
28
+ def [](val)
29
+ data[val]
30
+ end
31
+
32
+ def jsonapi_render_options(opts = {})
33
+ opts[:meta] ||= {}
34
+ opts[:expose] ||= {}
35
+ opts[:expose][:context] = JsonapiCompliable.context[:object]
36
+ opts
37
+ end
38
+
39
+ def to_jsonapi(options = {})
40
+ options = jsonapi_render_options(options)
41
+ Renderer.new(self, options).to_jsonapi
42
+ end
43
+
44
+ def to_json(options = {})
45
+ Renderer.new(self, options).to_json
46
+ end
47
+
48
+ def to_xml(options = {})
49
+ Renderer.new(self, options).to_xml
50
+ end
51
+
52
+ def data
53
+ @data ||= begin
54
+ records = @scope.resolve
55
+ if records.empty? && raise_on_missing?
56
+ raise JsonapiCompliable::Errors::RecordNotFound
57
+ end
58
+ records = records[0] if single?
59
+ records
60
+ end
61
+ end
62
+ alias :to_a :data
63
+
64
+ def each(&blk)
65
+ to_a.each(&blk)
66
+ end
67
+
68
+ def stats
69
+ @stats ||= @scope.resolve_stats
70
+ end
71
+
72
+ def save(action: :create)
73
+ validator = persist do
74
+ @resource.persist_with_relationships \
75
+ @payload.meta(action: action),
76
+ @payload.attributes,
77
+ @payload.relationships
78
+ end
79
+ @data, success = validator.to_a
80
+ success
81
+ end
82
+
83
+ def destroy
84
+ validator = @resource.transaction do
85
+ model = @resource.destroy(@query.filters[:id])
86
+ model.instance_variable_set(:@__serializer_klass, @resource.serializer)
87
+ validator = ::JsonapiCompliable::Util::ValidationResponse.new \
88
+ model, @payload
89
+ validator.validate!
90
+ @resource.before_commit(model, :destroy)
91
+ validator
92
+ end
93
+ @data, success = validator.to_a
94
+ success
95
+ end
96
+
97
+ def update_attributes
98
+ save(action: :update)
99
+ end
100
+
101
+ def include_hash
102
+ @payload ? @payload.include_hash : @query.include_hash
103
+ end
104
+
105
+ def fields
106
+ query.fields
107
+ end
108
+
109
+ def extra_fields
110
+ query.extra_fields
111
+ end
112
+
113
+ private
114
+
115
+ def persist
116
+ @resource.transaction do
117
+ ::JsonapiCompliable::Util::Hooks.record do
118
+ model = yield
119
+ validator = ::JsonapiCompliable::Util::ValidationResponse.new \
120
+ model, @payload
121
+ validator.validate!
122
+ validator
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,19 @@
1
+ # If you're using Rails + responders gem to get respond_with
2
+ module JsonapiCompliable
3
+ module Responders
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include ActionController::MimeResponds
8
+ respond_to :json, :jsonapi, :xml, :api_json
9
+ end
10
+
11
+ # Override to avoid location url generation (for now)
12
+ def respond_with(*args, &blk)
13
+ opts = args.extract_options!
14
+ opts[:location] = nil
15
+ args << opts
16
+ super(*args, &blk)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,25 @@
1
+ module JsonapiCompliable
2
+ class Runner
3
+ attr_reader :params
4
+ include JsonapiCompliable::Base
5
+
6
+ def initialize(resource_class, params)
7
+ @resource_class = resource_class
8
+ @params = params
9
+ end
10
+
11
+ def jsonapi_resource
12
+ @jsonapi_resource ||= @resource_class.new
13
+ end
14
+
15
+ # Typically, this is 'self' of a controller
16
+ # We're overriding here so we can do stuff like
17
+ #
18
+ # JsonapiCompliable.with_context my_context, {} do
19
+ # Runner.new ...
20
+ # end
21
+ def jsonapi_context
22
+ JsonapiCompliable.context[:object]
23
+ end
24
+ end
25
+ end
@@ -1,112 +1,69 @@
1
1
  module JsonapiCompliable
2
- # A Scope wraps an underlying object. It modifies that object
3
- # using the corresponding Resource and Query, and how to resolve
4
- # that underlying object scope.
5
- #
6
- # @example Basic Controller usage
7
- # def index
8
- # base = Post.all
9
- # scope = jsonapi_scope(base)
10
- # scope.object == base # => true
11
- # scope.object = scope.object.where(active: true)
12
- #
13
- # # actually fires sql
14
- # scope.resolve #=> [#<Post ...>, #<Post ...>, etc]
15
- # end
16
2
  class Scope
17
- attr_reader :object, :unpaginated_object
3
+ attr_accessor :object, :unpaginated_object
18
4
 
19
- # @param object - The underlying, chainable base scope object
20
- # @param resource - The Resource that will process the object
21
- # @param query - The Query object for the current request
22
- # @param [Hash] opts Options to configure the Scope
23
- # @option opts [String] :namespace The nested relationship name
24
- # @option opts [Boolean] :filter Opt-out of filter scoping
25
- # @option opts [Boolean] :extra_fields Opt-out of extra_fields scoping
26
- # @option opts [Boolean] :sort Opt-out of sort scoping
27
- # @option opts [Boolean] :pagination Opt-out of pagination scoping
28
- # @option opts [Boolean] :default_paginate Opt-out of default pagination when not specified in request
29
5
  def initialize(object, resource, query, opts = {})
30
6
  @object = object
31
7
  @resource = resource
32
8
  @query = query
9
+ @opts = opts
33
10
 
34
- # Namespace for the 'outer' or 'main' resource is its type
35
- # For its relationships, its the relationship name
36
- # IOW when hitting /states, it's resource type 'states'
37
- # when hitting /authors?include=state its 'state'
38
- @namespace = opts.delete(:namespace) || resource.type
39
-
40
- apply_scoping(opts)
11
+ @object = @resource.around_scoping(@object, query_hash) do |scope|
12
+ apply_scoping(scope, opts)
13
+ end
41
14
  end
42
15
 
43
- # Resolve the requested stats. Returns hash like:
44
- #
45
- # { rating: { average: 5.5, maximum: 9 } }
46
- #
47
- # @return [Hash] the resolved stat info
48
- # @api private
49
16
  def resolve_stats
50
- Stats::Payload.new(@resource, query_hash, @unpaginated_object).generate
17
+ if query_hash[:stats]
18
+ Stats::Payload.new(@resource, @query, @unpaginated_object).generate
19
+ else
20
+ {}
21
+ end
51
22
  end
52
23
 
53
- # Resolve the scope. This is where SQL is actually fired, an HTTP
54
- # request is actually made, etc.
55
- #
56
- # Does nothing if the user requested zero results, ie /posts?page[size]=0
57
- #
58
- # First resolves the top-level resource, then processes each relevant sideload
59
- #
60
- # @see Resource#resolve
61
- # @return [Array] an array of resolved model instances
62
24
  def resolve
63
25
  if @query.zero_results?
64
26
  []
65
27
  else
66
28
  resolved = @resource.resolve(@object)
29
+ assign_serializer(resolved)
67
30
  yield resolved if block_given?
68
- sideload(resolved, query_hash[:include]) if query_hash[:include]
31
+ if @opts[:after_resolve]
32
+ @opts[:after_resolve].call(resolved)
33
+ end
34
+ sideload(resolved) unless @query.sideloads.empty?
69
35
  resolved
70
36
  end
71
37
  end
72
38
 
73
- # The slice of Query#to_hash for the current namespace
74
- # @see Query#to_hash
75
- # @api private
76
39
  def query_hash
77
- @query_hash ||= @query.to_hash[@namespace]
40
+ @query_hash ||= @query.to_hash
78
41
  end
79
42
 
80
43
  private
81
44
 
82
- def sideload(results, includes)
45
+ # Used to ensure the resource's serializer is used
46
+ # Not one derived through the usual jsonapi-rb logic
47
+ def assign_serializer(records)
48
+ records.each do |r|
49
+ serializer = @resource.serializer_for(r)
50
+ r.instance_variable_set(:@__serializer_klass, serializer)
51
+ end
52
+ end
53
+
54
+ def sideload(results)
83
55
  return if results == []
84
56
 
85
57
  concurrent = ::JsonapiCompliable.config.experimental_concurrency
86
58
  promises = []
87
59
 
88
- includes.each_pair do |name, nested|
89
- sideload = @resource.sideload(name)
90
-
91
- if sideload.nil?
92
- if JsonapiCompliable.config.raise_on_missing_sideload
93
- raise JsonapiCompliable::Errors::InvalidInclude
94
- .new(name, @resource.type)
95
- end
60
+ @query.sideloads.each_pair do |name, q|
61
+ sideload = @resource.class.sideload(name)
62
+ resolve_sideload = -> { sideload.resolve(results, q) }
63
+ if concurrent
64
+ promises << Concurrent::Promise.execute(&resolve_sideload)
96
65
  else
97
- namespace = Util::Sideload.namespace(@namespace, sideload.name)
98
- resolve_sideload = -> {
99
- begin
100
- sideload.resolve(results, @query, namespace)
101
- ensure
102
- ActiveRecord::Base.clear_active_connections! if defined?(ActiveRecord)
103
- end
104
- }
105
- if concurrent
106
- promises << Concurrent::Promise.execute(&resolve_sideload)
107
- else
108
- resolve_sideload.call
109
- end
66
+ resolve_sideload.call
110
67
  end
111
68
  end
112
69
 
@@ -124,16 +81,17 @@ module JsonapiCompliable
124
81
  end
125
82
  end
126
83
 
127
- def apply_scoping(opts)
84
+ def apply_scoping(scope, opts)
85
+ @object = scope
128
86
  add_scoping(nil, JsonapiCompliable::Scoping::DefaultFilter, opts)
129
87
  add_scoping(:filter, JsonapiCompliable::Scoping::Filter, opts)
130
- add_scoping(:extra_fields, JsonapiCompliable::Scoping::ExtraFields, opts)
131
88
  add_scoping(:sort, JsonapiCompliable::Scoping::Sort, opts)
132
- add_scoping(:paginate, JsonapiCompliable::Scoping::Paginate, opts, default: opts[:default_paginate])
89
+ add_scoping(:paginate, JsonapiCompliable::Scoping::Paginate, opts)
90
+ @object
133
91
  end
134
92
 
135
93
  def add_scoping(key, scoping_class, opts, default = {})
136
- @object = scoping_class.new(@resource, query_hash, @object, default).apply unless opts[key] == false
94
+ @object = scoping_class.new(@resource, query_hash, @object, opts).apply
137
95
  @unpaginated_object = @object unless key == :paginate
138
96
  end
139
97
  end
@@ -0,0 +1,29 @@
1
+ module JsonapiCompliable
2
+ class Scoping::ExtraAttributes < Scoping::Base
3
+ # Loop through all requested extra fields. If custom scoping
4
+ # logic is define for that field, run it. Otherwise, do nothing.
5
+ #
6
+ # @return the scope object we are chaining/modofying
7
+ def apply
8
+ each_extra_attribute do |callable|
9
+ @scope = callable.call(@scope, resource.context)
10
+ end
11
+
12
+ @scope
13
+ end
14
+
15
+ private
16
+
17
+ def each_extra_field
18
+ resource.extra_fields.each_pair do |name, callable|
19
+ if extra_fields.include?(name)
20
+ yield callable
21
+ end
22
+ end
23
+ end
24
+
25
+ def extra_fields
26
+ query_hash[:extra_fields][resource.type] || []
27
+ end
28
+ end
29
+ end