graphiti-rb 1.0.alpha.1

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 (95) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +20 -0
  6. data/.yardopts +2 -0
  7. data/Appraisals +11 -0
  8. data/CODE_OF_CONDUCT.md +49 -0
  9. data/Gemfile +12 -0
  10. data/Guardfile +32 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +75 -0
  13. data/Rakefile +15 -0
  14. data/bin/appraisal +17 -0
  15. data/bin/console +14 -0
  16. data/bin/rspec +17 -0
  17. data/bin/setup +8 -0
  18. data/gemfiles/rails_4.gemfile +17 -0
  19. data/gemfiles/rails_5.gemfile +17 -0
  20. data/graphiti.gemspec +34 -0
  21. data/lib/generators/jsonapi/resource_generator.rb +169 -0
  22. data/lib/generators/jsonapi/templates/application_resource.rb.erb +15 -0
  23. data/lib/generators/jsonapi/templates/controller.rb.erb +61 -0
  24. data/lib/generators/jsonapi/templates/create_request_spec.rb.erb +30 -0
  25. data/lib/generators/jsonapi/templates/destroy_request_spec.rb.erb +20 -0
  26. data/lib/generators/jsonapi/templates/index_request_spec.rb.erb +22 -0
  27. data/lib/generators/jsonapi/templates/resource.rb.erb +11 -0
  28. data/lib/generators/jsonapi/templates/resource_reads_spec.rb.erb +62 -0
  29. data/lib/generators/jsonapi/templates/resource_writes_spec.rb.erb +63 -0
  30. data/lib/generators/jsonapi/templates/show_request_spec.rb.erb +21 -0
  31. data/lib/generators/jsonapi/templates/update_request_spec.rb.erb +34 -0
  32. data/lib/graphiti-rb.rb +1 -0
  33. data/lib/graphiti.rb +121 -0
  34. data/lib/graphiti/adapters/abstract.rb +516 -0
  35. data/lib/graphiti/adapters/active_record.rb +6 -0
  36. data/lib/graphiti/adapters/active_record/base.rb +249 -0
  37. data/lib/graphiti/adapters/active_record/belongs_to_sideload.rb +17 -0
  38. data/lib/graphiti/adapters/active_record/has_many_sideload.rb +17 -0
  39. data/lib/graphiti/adapters/active_record/has_one_sideload.rb +17 -0
  40. data/lib/graphiti/adapters/active_record/inferrence.rb +12 -0
  41. data/lib/graphiti/adapters/active_record/many_to_many_sideload.rb +30 -0
  42. data/lib/graphiti/adapters/null.rb +236 -0
  43. data/lib/graphiti/base.rb +70 -0
  44. data/lib/graphiti/configuration.rb +21 -0
  45. data/lib/graphiti/context.rb +16 -0
  46. data/lib/graphiti/deserializer.rb +208 -0
  47. data/lib/graphiti/errors.rb +309 -0
  48. data/lib/graphiti/extensions/boolean_attribute.rb +33 -0
  49. data/lib/graphiti/extensions/extra_attribute.rb +70 -0
  50. data/lib/graphiti/extensions/temp_id.rb +26 -0
  51. data/lib/graphiti/filter_operators.rb +25 -0
  52. data/lib/graphiti/hash_renderer.rb +57 -0
  53. data/lib/graphiti/jsonapi_serializable_ext.rb +50 -0
  54. data/lib/graphiti/query.rb +251 -0
  55. data/lib/graphiti/rails.rb +28 -0
  56. data/lib/graphiti/railtie.rb +74 -0
  57. data/lib/graphiti/renderer.rb +60 -0
  58. data/lib/graphiti/resource.rb +110 -0
  59. data/lib/graphiti/resource/configuration.rb +239 -0
  60. data/lib/graphiti/resource/dsl.rb +138 -0
  61. data/lib/graphiti/resource/interface.rb +32 -0
  62. data/lib/graphiti/resource/polymorphism.rb +68 -0
  63. data/lib/graphiti/resource/sideloading.rb +102 -0
  64. data/lib/graphiti/resource_proxy.rb +127 -0
  65. data/lib/graphiti/responders.rb +19 -0
  66. data/lib/graphiti/runner.rb +25 -0
  67. data/lib/graphiti/scope.rb +98 -0
  68. data/lib/graphiti/scoping/base.rb +99 -0
  69. data/lib/graphiti/scoping/default_filter.rb +58 -0
  70. data/lib/graphiti/scoping/extra_attributes.rb +29 -0
  71. data/lib/graphiti/scoping/filter.rb +93 -0
  72. data/lib/graphiti/scoping/filterable.rb +36 -0
  73. data/lib/graphiti/scoping/paginate.rb +87 -0
  74. data/lib/graphiti/scoping/sort.rb +64 -0
  75. data/lib/graphiti/sideload.rb +281 -0
  76. data/lib/graphiti/sideload/belongs_to.rb +34 -0
  77. data/lib/graphiti/sideload/has_many.rb +16 -0
  78. data/lib/graphiti/sideload/has_one.rb +9 -0
  79. data/lib/graphiti/sideload/many_to_many.rb +24 -0
  80. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +108 -0
  81. data/lib/graphiti/stats/dsl.rb +89 -0
  82. data/lib/graphiti/stats/payload.rb +49 -0
  83. data/lib/graphiti/types.rb +172 -0
  84. data/lib/graphiti/util/attribute_check.rb +88 -0
  85. data/lib/graphiti/util/field_params.rb +16 -0
  86. data/lib/graphiti/util/hash.rb +51 -0
  87. data/lib/graphiti/util/hooks.rb +33 -0
  88. data/lib/graphiti/util/include_params.rb +39 -0
  89. data/lib/graphiti/util/persistence.rb +219 -0
  90. data/lib/graphiti/util/relationship_payload.rb +64 -0
  91. data/lib/graphiti/util/serializer_attributes.rb +97 -0
  92. data/lib/graphiti/util/sideload.rb +33 -0
  93. data/lib/graphiti/util/validation_response.rb +78 -0
  94. data/lib/graphiti/version.rb +3 -0
  95. metadata +317 -0
@@ -0,0 +1,16 @@
1
+ class Graphiti::Sideload::HasMany < Graphiti::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 Graphiti::Sideload::HasOne < Graphiti::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 Graphiti::Sideload::ManyToMany < Graphiti::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 Graphiti::Sideload::PolymorphicBelongsTo < Graphiti::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 :field_name
17
+
18
+ def initialize(field_name)
19
+ @field_name = field_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.field_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 = ::Graphiti::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
@@ -0,0 +1,89 @@
1
+ module Graphiti
2
+ module Stats
3
+ # Provides an easier interface to stats scoping.
4
+ #
5
+ # Used within Resource DSL:
6
+ #
7
+ # allow_stat total: [:count] do
8
+ # # ... eval'd in Stats::DSL context! ...
9
+ # end
10
+ #
11
+ # This allows us to define arbitrary stats:
12
+ #
13
+ # allow_stat total: [:count] do
14
+ # standard_deviation { |scope, attr| ... }
15
+ # end
16
+ #
17
+ # And use convenience methods:
18
+ #
19
+ # allow_stat :rating do
20
+ # count!
21
+ # average!
22
+ # end
23
+ #
24
+ # @see Resource.allow_stat
25
+ # @attr_reader [Symbol] name the stat, e.g. :total
26
+ # @attr_reader [Hash] calculations procs for various metrics
27
+ class DSL
28
+ attr_reader :name, :calculations
29
+
30
+ # @param [Adapters::Abstract] adapter the Resource adapter
31
+ # @param [Symbol, Hash] config example: +:total+ or +{ total: [:count] }+
32
+ def initialize(adapter, config)
33
+ config = { config => [] } if config.is_a?(Symbol)
34
+
35
+ @adapter = adapter
36
+ @calculations = {}
37
+ @name = config.keys.first
38
+ Array(config.values.first).each { |c| send(:"#{c}!") }
39
+ end
40
+
41
+ # Used for defining arbitrary stats within the DSL:
42
+ #
43
+ # allow_stat :total do
44
+ # standard_deviation { |scope, attr| ... }
45
+ # end
46
+ #
47
+ # ...will hit +method_missing+ and store the proc for future reference.
48
+ # @api private
49
+ def method_missing(meth, *args, &blk)
50
+ @calculations[meth] = blk
51
+ end
52
+
53
+ # Grab a calculation proc. Raises error if no corresponding stat
54
+ # has been configured.
55
+ #
56
+ # @param [String, Symbol] name the name of the calculation, e.g. +:total+
57
+ # @return [Proc] the proc to run the calculation
58
+ def calculation(name)
59
+ callable = @calculations[name] || @calculations[name.to_sym]
60
+ callable || raise(Errors::StatNotFound.new(@name, name))
61
+ end
62
+
63
+ # Convenience method for default :count proc
64
+ def count!
65
+ @calculations[:count] = @adapter.method(:count)
66
+ end
67
+
68
+ # Convenience method for default :sum proc
69
+ def sum!
70
+ @calculations[:sum] = @adapter.method(:sum)
71
+ end
72
+
73
+ # Convenience method for default :average proc
74
+ def average!
75
+ @calculations[:average] = @adapter.method(:average)
76
+ end
77
+
78
+ # Convenience method for default :maximum proc
79
+ def maximum!
80
+ @calculations[:maximum] = @adapter.method(:maximum)
81
+ end
82
+
83
+ # Convenience method for default :minimum proc
84
+ def minimum!
85
+ @calculations[:minimum] = @adapter.method(:minimum)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,49 @@
1
+ module Graphiti
2
+ module Stats
3
+ # Generate the stats payload so we can return it in the response.
4
+ #
5
+ # {
6
+ # data: [...],
7
+ # meta: { stats: the_generated_payload }
8
+ # }
9
+ #
10
+ # For example:
11
+ #
12
+ # {
13
+ # data: [...],
14
+ # meta: { stats: { total: { count: 100 } } }
15
+ # }
16
+ class Payload
17
+ def initialize(resource, query, scope)
18
+ @resource = resource
19
+ @query = query
20
+ @scope = scope
21
+ end
22
+
23
+ # Generate the payload for +{ meta: { stats: { ... } } }+
24
+ # Loops over all calculations, computes then, and gives back
25
+ # a hash of stats and their results.
26
+ # @return [Hash] the generated payload
27
+ def generate
28
+ {}.tap do |stats|
29
+ @query.stats.each_pair do |name, calculation|
30
+ stats[name] = {}
31
+
32
+ each_calculation(name, calculation) do |calc, function|
33
+ stats[name][calc] = function.call(@scope, name)
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def each_calculation(name, calculations)
42
+ calculations.each do |calc|
43
+ function = @resource.stat(name, calc)
44
+ yield calc, function
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,172 @@
1
+ module Graphiti
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 decimal*
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
+ big_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