jsonapionify 0.11.11 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +8 -8
  2. data/README.md +3 -0
  3. data/examples/example_api.rb +80 -0
  4. data/jsonapionify.gemspec +1 -0
  5. data/lib/jsonapionify/api/action/documentation.rb +2 -2
  6. data/lib/jsonapionify/api/attribute.rb +3 -2
  7. data/lib/jsonapionify/api/context.rb +8 -3
  8. data/lib/jsonapionify/api/context_delegate.rb +6 -0
  9. data/lib/jsonapionify/api/relationship.rb +5 -10
  10. data/lib/jsonapionify/api/relationship/many.rb +46 -14
  11. data/lib/jsonapionify/api/relationship/one.rb +31 -8
  12. data/lib/jsonapionify/api/resource.rb +3 -6
  13. data/lib/jsonapionify/api/resource/builders/fields_builder.rb +2 -2
  14. data/lib/jsonapionify/api/resource/builders/relationship_builder.rb +3 -2
  15. data/lib/jsonapionify/api/resource/builders/relationships_builder.rb +3 -2
  16. data/lib/jsonapionify/api/resource/caching.rb +1 -4
  17. data/lib/jsonapionify/api/resource/callbacks.rb +60 -0
  18. data/lib/jsonapionify/api/resource/caller.rb +8 -7
  19. data/lib/jsonapionify/api/resource/defaults/actions.rb +15 -12
  20. data/lib/jsonapionify/api/resource/defaults/hooks.rb +10 -57
  21. data/lib/jsonapionify/api/resource/defaults/options.rb +3 -8
  22. data/lib/jsonapionify/api/resource/defaults/params.rb +0 -2
  23. data/lib/jsonapionify/api/resource/defaults/request_contexts.rb +24 -29
  24. data/lib/jsonapionify/api/resource/defaults/response_contexts.rb +11 -18
  25. data/lib/jsonapionify/api/resource/definitions/actions.rb +38 -22
  26. data/lib/jsonapionify/api/resource/definitions/attributes.rb +2 -2
  27. data/lib/jsonapionify/api/resource/definitions/helpers.rb +1 -1
  28. data/lib/jsonapionify/api/resource/definitions/includes.rb +16 -0
  29. data/lib/jsonapionify/api/resource/definitions/pagination.rb +7 -11
  30. data/lib/jsonapionify/api/resource/definitions/params.rb +19 -26
  31. data/lib/jsonapionify/api/resource/definitions/relationships.rb +3 -3
  32. data/lib/jsonapionify/api/resource/definitions/request_headers.rb +14 -16
  33. data/lib/jsonapionify/api/resource/definitions/response_headers.rb +2 -1
  34. data/lib/jsonapionify/api/resource/definitions/scopes.rb +17 -6
  35. data/lib/jsonapionify/api/resource/definitions/sorting.rb +8 -7
  36. data/lib/jsonapionify/api/resource/error_handling.rb +3 -2
  37. data/lib/jsonapionify/api/resource/exec.rb +3 -1
  38. data/lib/jsonapionify/api/resource/includer.rb +20 -51
  39. data/lib/jsonapionify/api/response.rb +3 -1
  40. data/lib/jsonapionify/callbacks.rb +10 -7
  41. data/lib/jsonapionify/destructured_proc.rb +27 -0
  42. data/lib/jsonapionify/structure/collections/base.rb +20 -8
  43. data/lib/jsonapionify/structure/objects/base.rb +11 -2
  44. data/lib/jsonapionify/structure/objects/resource_identifier.rb +9 -14
  45. data/lib/jsonapionify/version.rb +1 -1
  46. metadata +20 -2
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- NGNhMGNhNzcwZDMxOGZkYTA3MzBhZWIzNDhiZDlhODEyNjUwYzZkMw==
4
+ NWMyMjExNTU5ZmE5NzgyMWYyN2E4ZjFmZGFmY2FkN2I0MTZlZDliYw==
5
5
  data.tar.gz: !binary |-
6
- MzFmOWY1MDZkODE2ODU4NGZjYjYzNmQ3ZWIyNmJhNGQ4ZWFjNTExMA==
6
+ NDM2YzE2YjRhMzIzNzNhNWZlNGZmNmY0NmExOWQ5NTUwOGI4ZmMzMQ==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- N2MzN2UyZjYzYjMwMjE4NTdmYjFjMTgyYzdjNDA0NDUwNTlmMDliOWQ0ZDUy
10
- MTk2MTBhZjA0MWNkNThiMDhjZmVhYTRiNzBlYmNkOGE3ZWY2OWZkZGM1ZmNj
11
- ODVmMzFlNGRmMDJmYTQ0ZWJlZjNmOTM5YTFhYjYyYTJjZmVhYjU=
9
+ ZTM3ZTczNTQzZTUwN2Q1MmVmNTAwNWI0ZGEzN2E2MjYwMTM4MzhjZTJlYjVk
10
+ NTNiYTEyZWZjMTgwN2M0OWY5MzBmMjY1MGNhNzFmNjUxOTM1ZTkxYzZiYjU5
11
+ MWQ5Y2Q0ZmY5MzYyOWRiZDFjYjJhMjRjYjgwNDRmYjBlMGY3MTk=
12
12
  data.tar.gz: !binary |-
13
- ODI2NWRiMWZhMTYxMWIzNmQwOTg4MmRlMTI0MjJhZDljNTM0NzNmZTY5Y2Zm
14
- MjA4OGJlOTFkZDA5M2U5ZTkyMWU0NzgzNGI1OWVhMWEyYmRkNjM2ZTI3YjJj
15
- NTdkMjM3ZWZhYThhY2Y2MTRmMWVlZGIzZDU3ZmQ0ZjJkZGMzNTE=
13
+ OTI0Y2Q0NGZlZDA3MjYzMmVmNWE4OGVmYmExY2JlMDJiNjc4MzE5ODk5OGQ3
14
+ M2VmY2U4ZGUxZGI4ZjE3YzAxNjdiMjVkNzJmYWU5ZmZjOTczYzExMzFiMzI4
15
+ NzdhYzcxOTMwY2M5MjQ3ZjAwYjQxNzlhMzY0ZGRkYmY1MjIyODg=
data/README.md CHANGED
@@ -29,6 +29,9 @@ And then execute:
29
29
 
30
30
  ## Usage
31
31
 
32
+ Below is a high level overview of all the available methods. For for detail see
33
+ the yard documentation and the [examples](examples).
34
+
32
35
  ### APIs
33
36
 
34
37
  Api Definitions are the basis of JSONApi's DSL. They encompass resources that are available and can be run as standalone rack apps or be mounted within a rails app.
@@ -0,0 +1,80 @@
1
+ ## Apis
2
+
3
+ class ExampleApi < JSONAPIonify::Base
4
+
5
+ end
6
+
7
+ ## Resources
8
+ # Resources are what the api serves up to represent data within your application.
9
+
10
+ # **define_resource**<br />
11
+ # Calling `define_resource` on an Api class will define a resource.
12
+ # This will not only define the resource, but add routes to the api to access
13
+ # the resouce. By default only the **`read`** route (`GET /:resource/:id`) is
14
+ # added when you define a resource. Additional routes will have to be specified
15
+ # in the resource definition.
16
+ ExampleApi.define_resource :users do
17
+
18
+ ### Resource Setup
19
+ # Resources require a default setup that defines how the resource interacts
20
+ # with the data model.
21
+
22
+ # **scope**<br />
23
+ # Scope defines the model/object that the resource will represent. By default,
24
+ # it will try to find a class that matches the resource name. In this example
25
+ # resource, `:users` would implicitly look for a `User` class.
26
+ scope { User }
27
+
28
+ ### Attributes
29
+ # Attributes define the fields that can be set set on a resource on a request
30
+ # and what is provided back to the client in a response.
31
+
32
+ # **attribute**<br />
33
+ # Attributes can be defined within a resources definition using the `attribute`
34
+ # method within a resource definition block.
35
+ attribute :first_name, types.String, "The user's first name.", read: true, write: true, hidden: false, required: false do |name, instance, context|
36
+ instance.send(name)
37
+ end
38
+
39
+ # Basic attibutes only require a name, a type and a description. By default
40
+ # the resolution of the attribute is invoked by calling a method matching
41
+ # the attributes name on the instance of the resource.
42
+ attribute :last_name, types.String, "The users last name."
43
+
44
+ # Sometimes you wish to present information that may not be in the model.
45
+ # For this reason, you can define a block on the attribute which will tell
46
+ # the api how to fetch the value for that attibute. These attributes will
47
+ # default to being readonly attibutes.
48
+ attribute :full_name, types.String, "The user's full name." do |attr_name, instance, context|
49
+ "#{instance.first_name} #{instance.last_name}"
50
+ end
51
+
52
+ # Some attributes may be required for certain actions. In this case we
53
+ # can use the required keyword. `true` can be passed to require on all
54
+ # actions, or an action name or array of action names can be passed to
55
+ # target specific actions.
56
+ attribute :email, types.String, "The user's email address.", required: true
57
+
58
+ # Some attributes may be write only attributes, these attributes may
59
+ # include information we never want to send back in a response, such as
60
+ # passwords. `false` can be passed to prevent write on all actions, or an
61
+ # action name or array of action names can be passed to whitelist specific
62
+ # actions.
63
+ attribute :password, types.String, "The user's password", required: :create, read: false
64
+
65
+ # Some attributes may need to be read only attributes, such as timestamps
66
+ # or other attributes our application controls and should not be set by
67
+ # the end user. `false` can be passed to prevent write on all
68
+ # actions, or an action name or array of action names can be passed to
69
+ # whitelist specific actions.
70
+ attribute :updated_at, types.TimeString, "The last time the user was updated", write: false
71
+
72
+ # Some attributes may be tied to "expensive" calls. Rather than optimize
73
+ # your queries to allow for these attributes to be less expensive, we can
74
+ # simply hide the attributes by default. And they can only be used by
75
+ # utlizing [sparce fieldsets](http://jsonapi.org/format/#fetching-sparse-fieldsets).
76
+ # `true` can be passed to hide on all actions, or an action name or array of
77
+ # action names can be passed to target specific actions.
78
+ attribute :friends_count, types.Integer, "The number of friends for a user", write: false, hidden: [:list]
79
+
80
+ end
data/jsonapionify.gemspec CHANGED
@@ -36,6 +36,7 @@ Gem::Specification.new do |spec|
36
36
  spec.add_dependency 'mime-types'
37
37
 
38
38
  spec.add_development_dependency 'pry'
39
+ spec.add_development_dependency 'yard'
39
40
  spec.add_development_dependency 'pry-byebug'
40
41
  spec.add_development_dependency 'rocco'
41
42
  spec.add_development_dependency 'bundler', '~> 1.10'
@@ -63,8 +63,8 @@ module JSONAPIonify::Api
63
63
  end
64
64
  defs[:_is_example_] = Context.new(readonly: true) { true }
65
65
  defs[:collection] = Context.new(&collection_context)
66
- defs[:paginated_collection] = Context.new { |context| context.collection }
67
- defs[:instance] = Context.new(readonly: true) { |context| context.collection.first }
66
+ defs[:paginated_collection] = Context.new { |collection:| collection }
67
+ defs[:instance] = Context.new(readonly: true) { |collection:| collection.first }
68
68
  defs[:owner_context] = Context.new(readonly: true) { ContextDelegate::Mock.new } if defs.has_key? :owner_context
69
69
  end
70
70
  end
@@ -7,6 +7,7 @@ module JSONAPIonify::Api
7
7
 
8
8
  include Documentation
9
9
  using UnstrictProc
10
+ using JSONAPIonify::DestructuredProc
10
11
  attr_reader :name, :type, :description, :read, :write, :required, :hidden, :block
11
12
 
12
13
  def initialize(
@@ -29,7 +30,7 @@ module JSONAPIonify::Api
29
30
  @description = description&.freeze
30
31
  @example = example&.freeze
31
32
  @read = read&.freeze
32
- @write = write&.freeze
33
+ @write = (!block && write)&.freeze
33
34
  @required = required&.freeze
34
35
  @block = block&.freeze
35
36
  @writeable_actions = write
@@ -86,7 +87,7 @@ module JSONAPIonify::Api
86
87
  return example(example_id)
87
88
  end
88
89
  block = self.block || proc { |attr, i| i.send attr }
89
- type.dump block.unstrict.call(self.name, instance, context)
90
+ type.dump block.unstrict.destructure.call(self.name, instance, context)
90
91
  rescue JSONAPIonify::Types::DumpError => ex
91
92
  error_block =
92
93
  context.resource.class.error_definitions[:attribute_type_error]
@@ -1,18 +1,23 @@
1
1
  module JSONAPIonify::Api
2
2
  class Context
3
+ using JSONAPIonify::DestructuredProc
3
4
 
4
5
  def initialize(readonly: false, persisted: false, existing_context: nil, &block)
5
6
  @readonly = readonly
6
7
  @persisted = persisted
7
8
  @existing_context = existing_context
8
- @block = block
9
+ @block = block || proc {}
9
10
  end
10
11
 
11
12
  def call(instance, delegate)
12
13
  existing_context = @existing_context || proc {}
13
14
  existing_block = proc { existing_context.call(instance, delegate) }
14
- block = @block || proc {}
15
- instance.instance_exec(delegate, existing_block, &block)
15
+ begin
16
+ instance.instance_exec(delegate, existing_block, &@block.destructure(0))
17
+ rescue => e
18
+ e.backtrace.unshift @block.source_location.join(':') + ":in (context)"
19
+ raise e
20
+ end
16
21
  end
17
22
 
18
23
  def readonly?
@@ -60,5 +60,11 @@ module JSONAPIonify::Api
60
60
  to_s.chomp('>') << " memoed: #{@memo.keys.inspect}, persisted: #{@persisted_memo.keys.inspect}, overridden: #{@overrides.keys}" << '>'
61
61
  end
62
62
 
63
+ private
64
+
65
+ def __has_context?(name)
66
+ [@definitions, @overrides, @memo, @persisted_memo].map(&:keys).reduce(:|).include? name
67
+ end
68
+
63
69
  end
64
70
  end
@@ -27,12 +27,12 @@ module JSONAPIonify::Api
27
27
  rel.owner.new(request: request).exec { |c| c }
28
28
  end
29
29
 
30
- context(:owner_context, readonly: true, persisted: true) do |context|
31
- owner_context_proc.call(context.request)
30
+ context(:owner_context, readonly: true, persisted: true) do |request:|
31
+ owner_context_proc.call(request)
32
32
  end
33
33
 
34
- context(:owner, readonly: true, persisted: true) do |context|
35
- context.owner_context.instance
34
+ context(:owner, readonly: true, persisted: true) do |owner_context:|
35
+ owner_context.instance
36
36
  end
37
37
 
38
38
  context(:id, readonly: true, persisted: true) do
@@ -65,11 +65,10 @@ module JSONAPIonify::Api
65
65
 
66
66
  attr_reader :owner, :class_proc, :name, :resolve, :hidden
67
67
 
68
- def initialize(owner, name, resource: nil, includable: false, hidden: :list, resolve: proc { |n, o| o.send(n) }, &block)
68
+ def initialize(owner, name, resource: nil, hidden: :list, resolve: proc { |n, o| o.send(n) }, &block)
69
69
  @class_proc = block || proc {}
70
70
  @owner = owner
71
71
  @name = name
72
- @includable = includable
73
72
  @resource = resource || name
74
73
  @resolve = resolve
75
74
  @hidden = !!hidden && (hidden == true || Array.wrap(hidden))
@@ -99,9 +98,5 @@ module JSONAPIonify::Api
99
98
  owner.api.resource(@resource)
100
99
  end
101
100
 
102
- def includable?
103
- !!@includable
104
- end
105
-
106
101
  end
107
102
  end
@@ -1,5 +1,21 @@
1
1
  module JSONAPIonify::Api
2
2
  class Relationship::Many < Relationship
3
+ using JSONAPIonify::DestructuredProc
4
+
5
+ DEFAULT_REPLACE_COMMIT = proc { |scope:, request_instances:|
6
+ to_add = request_instances - scope
7
+ to_delete = scope - request_instances
8
+ to_delete.each { |instance| scope.delete(instance) }
9
+ scope.concat to_add
10
+ }
11
+
12
+ DEFAULT_ADD_COMMIT = proc { |scope:, request_instances:|
13
+ scope.concat request_instances
14
+ }
15
+
16
+ DEFAULT_REMOVE_COMMIT = proc { |scope:, request_instances:|
17
+ request_instances.each { |instance| scope.delete(instance) }
18
+ }
3
19
 
4
20
  prepend_class do
5
21
  rel = self.rel
@@ -15,13 +31,14 @@ module JSONAPIonify::Api
15
31
  cacheable: true,
16
32
  prepend: 'relationships'
17
33
  }
18
- define_action(:show, 'GET', **options, &block).response status: 200 do |context|
19
- context.response_object[:data] = build_resource_identifier_collection(collection: context.collection)
20
- context.response_object.to_json
34
+ define_action(:show, 'GET', **options, &block).response status: 200 do |collection:, response_object:|
35
+ response_object[:data] = build_resource_identifier_collection(collection: collection)
36
+ response_object.to_json
21
37
  end
22
38
  end
23
39
 
24
40
  define_singleton_method(:replace) do |content_type: nil, callbacks: true, &block|
41
+ block ||= DEFAULT_REPLACE_COMMIT
25
42
  options = {
26
43
  content_type: content_type,
27
44
  callbacks: callbacks,
@@ -29,13 +46,14 @@ module JSONAPIonify::Api
29
46
  prepend: 'relationships',
30
47
  example_input: :resource_identifier
31
48
  }
32
- define_action(:replace, 'PATCH', **options, &block).response status: 200 do |context|
33
- context.response_object[:data] = build_resource_identifier_collection(collection: context.collection)
34
- context.response_object.to_json
49
+ define_action(:replace, 'PATCH', **options, &block).response status: 200 do |collection:, response_object:|
50
+ response_object[:data] = build_resource_identifier_collection(collection: collection)
51
+ response_object.to_json
35
52
  end
36
53
  end
37
54
 
38
55
  define_singleton_method(:add) do |content_type: nil, callbacks: true, &block|
56
+ block ||= DEFAULT_ADD_COMMIT
39
57
  options = {
40
58
  content_type: content_type,
41
59
  callbacks: callbacks,
@@ -43,13 +61,14 @@ module JSONAPIonify::Api
43
61
  prepend: 'relationships',
44
62
  example_input: :resource_identifier
45
63
  }
46
- define_action(:add, 'POST', **options, &block).response status: 200 do |context|
47
- context.response_object[:data] = build_resource_identifier_collection(collection: context.collection)
48
- context.response_object.to_json
64
+ define_action(:add, 'POST', **options, &block).response status: 200 do |collection:, response_object:|
65
+ response_object[:data] = build_resource_identifier_collection(collection: collection)
66
+ response_object.to_json
49
67
  end
50
68
  end
51
69
 
52
70
  define_singleton_method(:remove) do |content_type: nil, callbacks: true, &block|
71
+ block ||= DEFAULT_REMOVE_COMMIT
53
72
  options = {
54
73
  content_type: content_type,
55
74
  callbacks: callbacks,
@@ -58,14 +77,27 @@ module JSONAPIonify::Api
58
77
  example_input: :resource_identifier
59
78
  }
60
79
  options[:prepend] = 'relationships'
61
- define_action(:remove, 'DELETE', **options, &block).response status: 200 do |context|
62
- context.response_object[:data] = build_resource_identifier_collection(collection: context.collection)
63
- context.response_object.to_json
80
+ define_action(:remove, 'DELETE', **options, &block).response status: 200 do |collection:, response_object:|
81
+ response_object[:data] = build_resource_identifier_collection(collection: collection)
82
+ response_object.to_json
64
83
  end
65
84
  end
66
85
 
67
- context :scope do |context|
68
- instance_exec(rel.name, context.owner, context, &rel.resolve)
86
+ context :scope do |context, owner:|
87
+ instance_exec(rel.name, owner, context, &rel.resolve.destructure)
88
+ end
89
+
90
+ after :commit_add, :commit_remove, :commit_replace do |owner:|
91
+ if defined?(ActiveRecord) && owner.is_a?(ActiveRecord::Base)
92
+ # Collect Errors
93
+ if owner.errors.present?
94
+ owner.errors.messages.each do |attr, messages|
95
+ messages.each do |message|
96
+ error :invalid_attribute, attr, message
97
+ end
98
+ end
99
+ end
100
+ end
69
101
  end
70
102
 
71
103
  show
@@ -1,5 +1,14 @@
1
1
  module JSONAPIonify::Api
2
2
  class Relationship::One < Relationship
3
+ using JSONAPIonify::DestructuredProc
4
+
5
+ DEFAULT_REPLACE_COMMIT = proc { |owner:, request_instance:|
6
+ # Set the association
7
+ owner.send "#{self.class.rel.name}=", request_instance
8
+
9
+ # Save the instance
10
+ owner.save if owner.respond_to? :save
11
+ }
3
12
 
4
13
  prepend_class do
5
14
  rel = self.rel
@@ -16,27 +25,41 @@ module JSONAPIonify::Api
16
25
  cacheable: true,
17
26
  prepend: 'relationships'
18
27
  }
19
- define_action(:show, 'GET', **options, &block).response status: 200 do |context|
20
- context.response_object[:data] = build_resource_identifier(instance: context.instance)
21
- context.response_object.to_json
28
+ define_action(:show, 'GET', **options, &block).response status: 200 do |response_object:, instance:|
29
+ response_object[:data] = build_resource_identifier(instance: instance)
30
+ response_object.to_json
22
31
  end
23
32
  end
24
33
 
25
34
  define_singleton_method(:replace) do |content_type: nil, callbacks: true, &block|
35
+ block ||= DEFAULT_REPLACE_COMMIT
26
36
  options = {
27
37
  content_type: content_type,
28
38
  callbacks: callbacks,
29
39
  cacheable: false,
30
40
  prepend: 'relationships'
31
41
  }
32
- define_action(:replace, 'PATCH', **options, &block).response status: 200 do |context|
33
- context.response_object[:data] = build_resource_identifier(instance: context.instance)
34
- context.response_object.to_json
42
+ define_action(:replace, 'PATCH', **options, &block).response status: 200 do |response_object:, instance:|
43
+ response_object[:data] = build_resource_identifier(instance: instance)
44
+ response_object.to_json
35
45
  end
36
46
  end
37
47
 
38
- context :instance do |context|
39
- instance_exec rel.name, context.owner, context, &rel.resolve
48
+ context :instance do |context, owner:|
49
+ instance_exec(rel.name, owner, context, &rel.resolve.destructure)
50
+ end
51
+
52
+ after :commit_replace do |owner:|
53
+ if defined?(ActiveRecord) && owner.is_a?(ActiveRecord::Base)
54
+ # Collect Errors
55
+ if owner.errors.present?
56
+ owner.errors.messages.each do |attr, messages|
57
+ messages.each do |message|
58
+ error :invalid_attribute, attr, message
59
+ end
60
+ end
61
+ end
62
+ end
40
63
  end
41
64
 
42
65
  show
@@ -7,8 +7,9 @@ module JSONAPIonify::Api
7
7
  extend JSONAPIonify::Autoload
8
8
  autoload_all
9
9
 
10
- extend Definitions
10
+ include Callbacks
11
11
  extend ClassMethods
12
+ extend Definitions
12
13
  extend Documentation
13
14
 
14
15
  include ErrorHandling
@@ -44,7 +45,7 @@ module JSONAPIonify::Api
44
45
  cacheable: true,
45
46
  action: nil
46
47
  )
47
- context_overrides[:action_name] = action.name if action
48
+ context_overrides[:action_name] ||= action.name if action
48
49
  @__context = ContextDelegate.new(
49
50
  request,
50
51
  self,
@@ -61,9 +62,5 @@ module JSONAPIonify::Api
61
62
  extend Caching if cacheable
62
63
  end
63
64
 
64
- def action_name
65
- action&.name
66
- end
67
-
68
65
  end
69
66
  end