graphiti 1.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) 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 +121 -0
  33. data/lib/graphiti/adapters/abstract.rb +516 -0
  34. data/lib/graphiti/adapters/active_record.rb +6 -0
  35. data/lib/graphiti/adapters/active_record/base.rb +249 -0
  36. data/lib/graphiti/adapters/active_record/belongs_to_sideload.rb +17 -0
  37. data/lib/graphiti/adapters/active_record/has_many_sideload.rb +17 -0
  38. data/lib/graphiti/adapters/active_record/has_one_sideload.rb +17 -0
  39. data/lib/graphiti/adapters/active_record/inferrence.rb +12 -0
  40. data/lib/graphiti/adapters/active_record/many_to_many_sideload.rb +30 -0
  41. data/lib/graphiti/adapters/null.rb +236 -0
  42. data/lib/graphiti/base.rb +70 -0
  43. data/lib/graphiti/configuration.rb +21 -0
  44. data/lib/graphiti/context.rb +16 -0
  45. data/lib/graphiti/deserializer.rb +208 -0
  46. data/lib/graphiti/errors.rb +309 -0
  47. data/lib/graphiti/extensions/boolean_attribute.rb +33 -0
  48. data/lib/graphiti/extensions/extra_attribute.rb +70 -0
  49. data/lib/graphiti/extensions/temp_id.rb +26 -0
  50. data/lib/graphiti/filter_operators.rb +25 -0
  51. data/lib/graphiti/hash_renderer.rb +57 -0
  52. data/lib/graphiti/jsonapi_serializable_ext.rb +50 -0
  53. data/lib/graphiti/query.rb +251 -0
  54. data/lib/graphiti/rails.rb +28 -0
  55. data/lib/graphiti/railtie.rb +74 -0
  56. data/lib/graphiti/renderer.rb +60 -0
  57. data/lib/graphiti/resource.rb +110 -0
  58. data/lib/graphiti/resource/configuration.rb +239 -0
  59. data/lib/graphiti/resource/dsl.rb +138 -0
  60. data/lib/graphiti/resource/interface.rb +32 -0
  61. data/lib/graphiti/resource/polymorphism.rb +68 -0
  62. data/lib/graphiti/resource/sideloading.rb +102 -0
  63. data/lib/graphiti/resource_proxy.rb +127 -0
  64. data/lib/graphiti/responders.rb +19 -0
  65. data/lib/graphiti/runner.rb +25 -0
  66. data/lib/graphiti/scope.rb +98 -0
  67. data/lib/graphiti/scoping/base.rb +99 -0
  68. data/lib/graphiti/scoping/default_filter.rb +58 -0
  69. data/lib/graphiti/scoping/extra_attributes.rb +29 -0
  70. data/lib/graphiti/scoping/filter.rb +93 -0
  71. data/lib/graphiti/scoping/filterable.rb +36 -0
  72. data/lib/graphiti/scoping/paginate.rb +87 -0
  73. data/lib/graphiti/scoping/sort.rb +64 -0
  74. data/lib/graphiti/sideload.rb +281 -0
  75. data/lib/graphiti/sideload/belongs_to.rb +34 -0
  76. data/lib/graphiti/sideload/has_many.rb +16 -0
  77. data/lib/graphiti/sideload/has_one.rb +9 -0
  78. data/lib/graphiti/sideload/many_to_many.rb +24 -0
  79. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +108 -0
  80. data/lib/graphiti/stats/dsl.rb +89 -0
  81. data/lib/graphiti/stats/payload.rb +49 -0
  82. data/lib/graphiti/types.rb +172 -0
  83. data/lib/graphiti/util/attribute_check.rb +88 -0
  84. data/lib/graphiti/util/field_params.rb +16 -0
  85. data/lib/graphiti/util/hash.rb +51 -0
  86. data/lib/graphiti/util/hooks.rb +33 -0
  87. data/lib/graphiti/util/include_params.rb +39 -0
  88. data/lib/graphiti/util/persistence.rb +219 -0
  89. data/lib/graphiti/util/relationship_payload.rb +64 -0
  90. data/lib/graphiti/util/serializer_attributes.rb +97 -0
  91. data/lib/graphiti/util/sideload.rb +33 -0
  92. data/lib/graphiti/util/validation_response.rb +78 -0
  93. data/lib/graphiti/version.rb +3 -0
  94. metadata +316 -0
@@ -0,0 +1,138 @@
1
+ module Graphiti
2
+ class Resource
3
+ module DSL
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
7
+ def filter(name, *args, &blk)
8
+ opts = args.extract_options!
9
+
10
+ if att = get_attr(name, :filterable, raise_error: :only_unsupported)
11
+ aliases = [name, opts[:aliases]].flatten.compact
12
+ operators = FilterOperators.build(&blk)
13
+ config[:filters][name.to_sym] = {
14
+ aliases: aliases,
15
+ type: att[:type]
16
+ }.merge(operators.to_hash)
17
+ else
18
+ if type = args[0]
19
+ attribute name, type, only: [:filterable]
20
+ filter(name, opts, &blk)
21
+ else
22
+ raise Errors::ImplicitFilterTypeMissing.new(self, name)
23
+ end
24
+ end
25
+ end
26
+
27
+ def sort_all(&blk)
28
+ if block_given?
29
+ config[:_sort_all] = blk
30
+ else
31
+ config[:_sort_all]
32
+ end
33
+ end
34
+
35
+ def sort(name, *args, &blk)
36
+ opts = args.extract_options!
37
+
38
+ if get_attr(name, :sortable, raise_error: :only_unsupported)
39
+ config[:sorts][name] = blk
40
+ else
41
+ if type = args[0]
42
+ attribute name, type, only: [:sortable]
43
+ sort(name, opts, &blk)
44
+ else
45
+ raise Errors::ImplicitSortTypeMissing.new(self, name)
46
+ end
47
+ end
48
+ end
49
+
50
+ def paginate(&blk)
51
+ config[:pagination] = blk
52
+ end
53
+
54
+ def stat(symbol_or_hash, &blk)
55
+ dsl = Stats::DSL.new(adapter, symbol_or_hash)
56
+ dsl.instance_eval(&blk) if blk
57
+ config[:stats][dsl.name] = dsl
58
+ end
59
+
60
+ def default_filter(name = nil, &blk)
61
+ name ||= :__default
62
+ config[:default_filters][name.to_sym] = {
63
+ filter: blk
64
+ }
65
+ end
66
+
67
+ def before_commit(only: [:create, :update, :destroy], &blk)
68
+ Array(only).each do |verb|
69
+ config[:before_commit][verb] = blk
70
+ end
71
+ end
72
+
73
+ def attribute(name, type, options = {}, &blk)
74
+ raise Errors::TypeNotFound.new(self, name, type) unless Types[type]
75
+ attribute_option(options, :readable)
76
+ attribute_option(options, :writable)
77
+ attribute_option(options, :sortable)
78
+ attribute_option(options, :filterable)
79
+ options[:type] = type
80
+ options[:proc] = blk
81
+ config[:attributes][name] = options
82
+ apply_attributes_to_serializer
83
+ filter(name) if options[:filterable]
84
+ end
85
+
86
+ def extra_attribute(name, type, options = {}, &blk)
87
+ raise Errors::TypeNotFound.new(self, name, type) unless Types[type]
88
+ defaults = {
89
+ type: type,
90
+ proc: blk,
91
+ readable: true,
92
+ writable: false,
93
+ sortable: false,
94
+ filterable: false
95
+ }
96
+ options = defaults.merge(options)
97
+ config[:extra_attributes][name] = options
98
+ apply_extra_attributes_to_serializer
99
+ end
100
+
101
+ def all_attributes
102
+ attributes.merge(extra_attributes)
103
+ end
104
+
105
+ def apply_attributes_to_serializer
106
+ serializer.type(type)
107
+ Util::SerializerAttributes.new(self, attributes).apply
108
+ end
109
+ private :apply_attributes_to_serializer
110
+
111
+ def apply_extra_attributes_to_serializer
112
+ Util::SerializerAttributes.new(self, extra_attributes, true).apply
113
+ end
114
+
115
+ def attribute_option(options, name)
116
+ if options[name] != false
117
+ default = if only = options[:only]
118
+ Array(only).include?(name) ? true : false
119
+ elsif except = options[:except]
120
+ Array(except).include?(name) ? false : true
121
+ else
122
+ send(:"attributes_#{name}_by_default")
123
+ end
124
+ options[name] ||= default
125
+ end
126
+ end
127
+ private :attribute_option
128
+
129
+ def relationship_option(options, name)
130
+ if options[name] != false
131
+ options[name] ||= send(:"relationships_#{name}_by_default")
132
+ end
133
+ end
134
+ private :attribute_option
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,32 @@
1
+ module Graphiti
2
+ class Resource
3
+ module Interface
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
7
+ def all(params = {}, base_scope = nil)
8
+ _all(params, {}, base_scope)
9
+ end
10
+
11
+ def _all(params, opts, base_scope)
12
+ runner = Runner.new(self, params)
13
+ runner.proxy(base_scope, opts)
14
+ end
15
+
16
+ def find(params, base_scope = nil)
17
+ id = params[:data].try(:[], :id) || params.delete(:id)
18
+ params[:filter] ||= {}
19
+ params[:filter].merge!(id: id)
20
+
21
+ runner = Runner.new(self, params)
22
+ runner.proxy(base_scope, single: true, raise_on_missing: true)
23
+ end
24
+
25
+ def build(params, base_scope = nil)
26
+ runner = Runner.new(self, params)
27
+ runner.proxy(base_scope, single: true, raise_on_missing: true)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,68 @@
1
+ # For "Rails STI" behavior
2
+ # CreditCard.all # => [<Visa>, <Mastercard>, etc]
3
+ module Graphiti
4
+ class Resource
5
+ module Polymorphism
6
+ def self.prepended(klass)
7
+ klass.extend ClassMethods
8
+ end
9
+
10
+ def serializer_for(model)
11
+ if polymorphic_child?
12
+ serializer
13
+ else
14
+ child = self.class.resource_for_model(model)
15
+ child.serializer
16
+ end
17
+ end
18
+
19
+ def associate_all(parent, children, association_name, type)
20
+ children.each do |c|
21
+ associate(parent, c, association_name, type)
22
+ end
23
+ end
24
+
25
+ def associate(parent, child, association_name, type)
26
+ child_resource = self.class.resource_for_model(parent)
27
+ if child_resource.sideloads[association_name]
28
+ child_resource.adapter
29
+ .associate(parent, child, association_name, type)
30
+ end
31
+ end
32
+
33
+ module ClassMethods
34
+ def inherited(klass)
35
+ klass.type = nil
36
+ klass.model = klass.infer_model
37
+ klass.polymorphic_child = true
38
+ super
39
+ end
40
+
41
+ def sideload(name)
42
+ sl = super
43
+ if !polymorphic_child? && sl.nil?
44
+ children.each do |c|
45
+ break if sl = c.sideloads[name]
46
+ end
47
+ end
48
+ sl
49
+ end
50
+
51
+ def children
52
+ @children ||= polymorphic.map do |klass|
53
+ klass.is_a?(String) ? klass.safe_constantize : klass
54
+ end
55
+ end
56
+
57
+ def resource_for_model(model)
58
+ resource = children.find { |c| model.is_a?(c.model) }
59
+ if resource.nil?
60
+ raise Errors::PolymorphicChildNotFound.new(self, model)
61
+ else
62
+ resource
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,102 @@
1
+ module Graphiti
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(::Graphiti::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] ||= ::Graphiti::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 Graphiti
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] = Graphiti.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 Graphiti::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 = ::Graphiti::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
+ ::Graphiti::Util::Hooks.record do
118
+ model = yield
119
+ validator = ::Graphiti::Util::ValidationResponse.new \
120
+ model, @payload
121
+ validator.validate!
122
+ validator
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end