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,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