jsonapi_compliable 0.11.34 → 1.0.alpha.2

Sign up to get free protection for your applications and to get access to all the features.
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