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,34 @@
1
+ class JsonapiCompliable::Sideload::BelongsTo < JsonapiCompliable::Sideload
2
+ def type
3
+ :belongs_to
4
+ end
5
+
6
+ def load_params(parents, query)
7
+ query.to_hash.tap do |hash|
8
+ hash[:filter] ||= {}
9
+ hash[:filter][primary_key] = ids_for_parents(parents)
10
+ end
11
+ end
12
+
13
+ def assign_each(parent, children)
14
+ children.find { |c| c.send(primary_key) == parent.send(foreign_key) }
15
+ end
16
+
17
+ def ids_for_parents(parents)
18
+ parent_ids = parents.map(&foreign_key)
19
+ parent_ids.compact!
20
+ parent_ids.uniq!
21
+ parent_ids
22
+ end
23
+
24
+ def infer_foreign_key
25
+ if polymorphic_child?
26
+ parent.foreign_key
27
+ else
28
+ model = resource.model
29
+ namespace = namespace_for(model)
30
+ model_name = model.name.gsub("#{namespace}::", '')
31
+ :"#{model_name.underscore}_id"
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,16 @@
1
+ class JsonapiCompliable::Sideload::HasMany < JsonapiCompliable::Sideload
2
+ def type
3
+ :has_many
4
+ end
5
+
6
+ def load_params(parents, query)
7
+ query.to_hash.tap do |hash|
8
+ hash[:filter] ||= {}
9
+ hash[:filter][foreign_key] = ids_for_parents(parents)
10
+ end
11
+ end
12
+
13
+ def assign_each(parent, children)
14
+ children.select { |c| c.send(foreign_key) == parent.send(primary_key) }
15
+ end
16
+ end
@@ -0,0 +1,9 @@
1
+ class JsonapiCompliable::Sideload::HasOne < JsonapiCompliable::Sideload::HasMany
2
+ def type
3
+ :has_one
4
+ end
5
+
6
+ def assign_each(parent, children)
7
+ children.find { |c| c.send(foreign_key) == parent.send(primary_key) }
8
+ end
9
+ end
@@ -0,0 +1,24 @@
1
+ class JsonapiCompliable::Sideload::ManyToMany < JsonapiCompliable::Sideload
2
+ def type
3
+ :many_to_many
4
+ end
5
+
6
+ def through
7
+ foreign_key.keys.first
8
+ end
9
+
10
+ def true_foreign_key
11
+ foreign_key.values.first
12
+ end
13
+
14
+ def infer_foreign_key
15
+ raise 'You must explicitly pass :foreign_key for many-to-many relaitonships, or override in subclass to return a hash.'
16
+ end
17
+
18
+ def assign_each(parent, children)
19
+ children.select do |c|
20
+ match = ->(ct) { ct.send(true_foreign_key) == parent.send(primary_key) }
21
+ c.send(through).any?(&match)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,108 @@
1
+ class JsonapiCompliable::Sideload::PolymorphicBelongsTo < JsonapiCompliable::Sideload::BelongsTo
2
+ class Group
3
+ attr_reader :name, :calls
4
+
5
+ def initialize(name)
6
+ @name = name
7
+ @calls = []
8
+ end
9
+
10
+ def method_missing(name, *args, &blk)
11
+ @calls << [name, args, blk]
12
+ end
13
+ end
14
+
15
+ class Grouper
16
+ attr_reader :column_name
17
+
18
+ def initialize(column_name)
19
+ @column_name = column_name
20
+ @groups = []
21
+ end
22
+
23
+ def on(name, &blk)
24
+ group = Group.new(name)
25
+ @groups << group
26
+ group.belongs_to(name.to_s.underscore.to_sym)
27
+ group
28
+ end
29
+
30
+ def apply(sideload, resource_class)
31
+ @groups.each do |group|
32
+ group.calls.each do |call|
33
+ args = call[1]
34
+ opts = args.extract_options!
35
+ opts.merge! as: sideload.name,
36
+ parent: sideload,
37
+ group_name: group.name,
38
+ polymorphic_child: true
39
+ if !sideload.resource.class.abstract_class?
40
+ opts[:foreign_key] ||= sideload.foreign_key
41
+ opts[:primary_key] ||= sideload.primary_key
42
+ end
43
+ args << opts
44
+ resource_class.send(call[0], *args, &call[2])
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ class_attribute :grouper
51
+ attr_accessor :children
52
+ self.grouper = Grouper.new(:default)
53
+
54
+ def type
55
+ :polymorphic_belongs_to
56
+ end
57
+
58
+ def infer_foreign_key
59
+ :"#{name}_id"
60
+ end
61
+
62
+ def self.group_by(name, &blk)
63
+ self.grouper = Grouper.new(name)
64
+ self.grouper.instance_eval(&blk)
65
+ end
66
+
67
+ def initialize(name, opts)
68
+ super
69
+ self.children = {}
70
+ grouper.apply(self, parent_resource_class)
71
+ end
72
+
73
+ def child_for_type(type)
74
+ children.values.find do |sideload|
75
+ sideload.resource.type == type
76
+ end
77
+ end
78
+
79
+ def resolve(parents, query)
80
+ parents.group_by(&grouper.column_name).each_pair do |group_name, group|
81
+ next if group_name.nil?
82
+
83
+ match = ->(name, sl) { sl.group_name == group_name.to_sym }
84
+ if child = children.find(&match)
85
+ sideload = child[1]
86
+ query = remove_invalid_sideloads(sideload.resource, query)
87
+ sideload.resolve(group, query)
88
+ else
89
+ err = ::JsonapiCompliable::Errors::PolymorphicChildNotFound
90
+ raise err.new(self, group_name)
91
+ end
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ # We may be requesting a relationship that some subclasses support,
98
+ # but not others. Remove anything we don't support.
99
+ def remove_invalid_sideloads(resource, query)
100
+ query = query.dup
101
+ query.sideloads.each_pair do |key, value|
102
+ unless resource.class.sideload(key)
103
+ query.sideloads.delete(key)
104
+ end
105
+ end
106
+ query
107
+ end
108
+ end
@@ -14,12 +14,9 @@ module JsonapiCompliable
14
14
  # meta: { stats: { total: { count: 100 } } }
15
15
  # }
16
16
  class Payload
17
- # @param [Resource] resource the resource instance
18
- # @param [Hash] query_hash the Query#to_hash for the current resource
19
- # @param scope the scope we are chaining/modifying
20
- def initialize(resource, query_hash, scope)
17
+ def initialize(resource, query, scope)
21
18
  @resource = resource
22
- @query_hash = query_hash[:stats]
19
+ @query = query
23
20
  @scope = scope
24
21
  end
25
22
 
@@ -29,12 +26,11 @@ module JsonapiCompliable
29
26
  # @return [Hash] the generated payload
30
27
  def generate
31
28
  {}.tap do |stats|
32
- @query_hash.each_pair do |name, calculation|
29
+ @query.stats.each_pair do |name, calculation|
33
30
  stats[name] = {}
34
31
 
35
32
  each_calculation(name, calculation) do |calc, function|
36
- args = function.arity == 3 ? [@scope, name, @resource] : [@scope, name]
37
- stats[name][calc] = function.call(*args)
33
+ stats[name][calc] = function.call(@scope, name)
38
34
  end
39
35
  end
40
36
  end
@@ -0,0 +1,172 @@
1
+ module JsonapiCompliable
2
+ class Types
3
+ def self.create(primitive, &blk)
4
+ definition = Dry::Types::Definition.new(primitive)
5
+ definition.constructor(&blk)
6
+ end
7
+
8
+ WriteDateTime = create(::DateTime) do |input|
9
+ if input.is_a?(::Date) || input.is_a?(::Time)
10
+ input = ::DateTime.parse(input.to_s)
11
+ end
12
+ input = Dry::Types['json.date_time'][input]
13
+ Dry::Types['strict.date_time'][input] if input
14
+ end
15
+
16
+ ReadDateTime = create(::DateTime) do |input|
17
+ if input.is_a?(::Date) || input.is_a?(::Time)
18
+ input = ::DateTime.parse(input.to_s)
19
+ end
20
+ input = Dry::Types['json.date_time'][input]
21
+ Dry::Types['strict.date_time'][input].iso8601 if input
22
+ end
23
+
24
+ PresentParamsDateTime = create(::DateTime) do |input|
25
+ input = Dry::Types['params.date_time'][input]
26
+ Dry::Types['strict.date_time'][input]
27
+ end
28
+
29
+ Date = create(::Date) do |input|
30
+ input = ::Date.parse(input.to_s) if input.is_a?(::Time)
31
+ input = Dry::Types['json.date'][input]
32
+ Dry::Types['strict.date'][input] if input
33
+ end
34
+
35
+ PresentDate = create(::Date) do |input|
36
+ input = ::Date.parse(input.to_s) if input.is_a?(::Time)
37
+ input = Dry::Types['json.date'][input]
38
+ Dry::Types['strict.date'][input]
39
+ end
40
+
41
+ Bool = create(nil) do |input|
42
+ input = Dry::Types['params.bool'][input]
43
+ Dry::Types['strict.bool'][input] if input
44
+ end
45
+
46
+ PresentBool = create(nil) do |input|
47
+ input = Dry::Types['params.bool'][input]
48
+ Dry::Types['strict.bool'][input]
49
+ end
50
+
51
+ Integer = create(::Integer) do |input|
52
+ Dry::Types['coercible.integer'][input] if input
53
+ end
54
+
55
+ # The Float() check here is to ensure we have a number
56
+ # Otherwise BigDecimal('foo') *will return a decima;*
57
+ ParamDecimal = create(::BigDecimal) do |input|
58
+ Float(input)
59
+ input = Dry::Types['coercible.decimal'][input]
60
+ Dry::Types['strict.decimal'][input]
61
+ end
62
+
63
+ PresentInteger = create(::Integer) do |input|
64
+ Dry::Types['coercible.integer'][input]
65
+ end
66
+
67
+ Float = create(::Float) do |input|
68
+ Dry::Types['coercible.float'][input] if input
69
+ end
70
+
71
+ PresentParamsHash = create(::Hash) do |input|
72
+ Dry::Types['params.hash'][input].deep_symbolize_keys
73
+ end
74
+
75
+ def self.map
76
+ @map ||= begin
77
+ hash = {
78
+ integer_id: {
79
+ canonical_name: :integer,
80
+ params: Dry::Types['coercible.integer'],
81
+ read: Dry::Types['coercible.string'],
82
+ write: Dry::Types['coercible.integer']
83
+ },
84
+ string: {
85
+ params: Dry::Types['coercible.string'],
86
+ read: Dry::Types['coercible.string'],
87
+ write: Dry::Types['coercible.string']
88
+ },
89
+ integer: {
90
+ params: PresentInteger,
91
+ read: Integer,
92
+ write: Integer
93
+ },
94
+ decimal: {
95
+ params: ParamDecimal,
96
+ read: Dry::Types['json.decimal'],
97
+ write: Dry::Types['json.decimal']
98
+ },
99
+ float: {
100
+ params: Dry::Types['coercible.float'],
101
+ read: Float,
102
+ write: Float
103
+ },
104
+ boolean: {
105
+ params: PresentBool,
106
+ read: Bool,
107
+ write: Bool
108
+ },
109
+ date: {
110
+ params: PresentDate,
111
+ read: Date,
112
+ write: Date
113
+ },
114
+ datetime: {
115
+ params: PresentParamsDateTime,
116
+ read: ReadDateTime,
117
+ write: WriteDateTime
118
+ },
119
+ hash: {
120
+ params: PresentParamsHash,
121
+ read: Dry::Types['strict.hash'],
122
+ write: Dry::Types['strict.hash']
123
+ },
124
+ array: {
125
+ params: Dry::Types['strict.array'],
126
+ read: Dry::Types['strict.array'],
127
+ write: Dry::Types['strict.array']
128
+ }
129
+ }
130
+
131
+ hash.each_pair do |k, v|
132
+ hash[k][:canonical_name] ||= k
133
+ end
134
+
135
+ arrays = {}
136
+ hash.each_pair do |name, map|
137
+ arrays[:"array_of_#{name.to_s.pluralize}"] = {
138
+ canonical_name: name,
139
+ params: Dry::Types['strict.array'].of(map[:params]),
140
+ read: Dry::Types['strict.array'].of(map[:read]),
141
+ test: Dry::Types['strict.array'].of(map[:test]),
142
+ write: Dry::Types['strict.array'].of(map[:write])
143
+ }
144
+ end
145
+ hash.merge!(arrays)
146
+
147
+ hash
148
+ end
149
+ end
150
+
151
+ def self.[](key)
152
+ map[key.to_sym]
153
+ end
154
+
155
+ def self.[]=(key, value)
156
+ unless value.is_a?(Hash)
157
+ value = {
158
+ read: value,
159
+ params: value,
160
+ test: value
161
+ }
162
+ end
163
+ map[key.to_sym] = value
164
+ end
165
+
166
+ def self.name_for(key)
167
+ key = key.to_sym
168
+ type = map[key]
169
+ type[:canonical_name]
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,88 @@
1
+ # Private, tested in resource specs
2
+ module JsonapiCompliable
3
+ module Util
4
+ class AttributeCheck
5
+ attr_reader :resource, :name, :flag, :request, :raise_error
6
+
7
+ def self.run(resource, name, flag, request, raise_error)
8
+ new(resource, name, flag, request, raise_error).run
9
+ end
10
+
11
+ def initialize(resource, name, flag, request, raise_error)
12
+ @resource = resource
13
+ @name = name.to_sym
14
+ @flag = flag
15
+ @request = request
16
+ @raise_error = raise_error
17
+ end
18
+
19
+ def run
20
+ if attribute?
21
+ if supported?
22
+ if guarded?
23
+ if guard_passes?
24
+ attribute
25
+ else
26
+ maybe_raise(request: true, guard: attribute[flag])
27
+ end
28
+ else
29
+ attribute
30
+ end
31
+ else
32
+ maybe_raise(exists: true)
33
+ end
34
+ else
35
+ maybe_raise(exists: false)
36
+ end
37
+ end
38
+
39
+ def maybe_raise(opts = {})
40
+ default = { request: request, exists: true }
41
+ opts = default.merge(opts)
42
+ if raise_error?(opts[:exists])
43
+ raise error_class.new(resource, name, flag, opts)
44
+ else
45
+ false
46
+ end
47
+ end
48
+
49
+ def guard_passes?
50
+ !!resource.send(attribute[flag])
51
+ end
52
+
53
+ def guarded?
54
+ request? &&
55
+ attribute[flag].is_a?(Symbol) &&
56
+ attribute[flag] != :required
57
+ end
58
+
59
+ def error_class
60
+ Errors::AttributeError
61
+ end
62
+
63
+ def supported?
64
+ attribute[flag] != false
65
+ end
66
+
67
+ def attribute
68
+ resource.all_attributes[name]
69
+ end
70
+
71
+ def attribute?
72
+ !!attribute
73
+ end
74
+
75
+ def raise_error?(exists)
76
+ if raise_error == :only_unsupported
77
+ exists ? true : false
78
+ else
79
+ !!raise_error
80
+ end
81
+ end
82
+
83
+ def request?
84
+ !!request
85
+ end
86
+ end
87
+ end
88
+ end