jsonapionify 0.9.0 → 0.9.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 (84) hide show
  1. checksums.yaml +13 -5
  2. data/.rubocop.yml +1 -0
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +8 -0
  5. data/README.md +85 -3
  6. data/Rakefile +14 -0
  7. data/jsonapionify.gemspec +3 -0
  8. data/lib/jsonapionify/api/action.rb +84 -121
  9. data/lib/jsonapionify/api/attribute.rb +97 -20
  10. data/lib/jsonapionify/api/base/class_methods.rb +5 -4
  11. data/lib/jsonapionify/api/base/delegation.rb +20 -4
  12. data/lib/jsonapionify/api/base/doc_helper.rb +3 -3
  13. data/lib/jsonapionify/api/base/reloader.rb +1 -1
  14. data/lib/jsonapionify/api/base/resource_definitions.rb +28 -15
  15. data/lib/jsonapionify/api/base.rb +6 -0
  16. data/lib/jsonapionify/api/context.rb +18 -5
  17. data/lib/jsonapionify/api/context_delegate.rb +24 -7
  18. data/lib/jsonapionify/api/errors.rb +2 -0
  19. data/lib/jsonapionify/api/errors_object.rb +6 -5
  20. data/lib/jsonapionify/api/relationship/blocks.rb +1 -1
  21. data/lib/jsonapionify/api/relationship/many.rb +35 -11
  22. data/lib/jsonapionify/api/relationship/one.rb +17 -7
  23. data/lib/jsonapionify/api/relationship.rb +20 -6
  24. data/lib/jsonapionify/api/resource/builders.rb +81 -30
  25. data/lib/jsonapionify/api/resource/caching.rb +28 -0
  26. data/lib/jsonapionify/api/resource/caller.rb +61 -0
  27. data/lib/jsonapionify/api/resource/class_methods.rb +6 -2
  28. data/lib/jsonapionify/api/resource/defaults/actions.rb +47 -0
  29. data/lib/jsonapionify/api/resource/defaults/errors.rb +61 -15
  30. data/lib/jsonapionify/api/resource/defaults/hooks.rb +68 -0
  31. data/lib/jsonapionify/api/resource/defaults/options.rb +16 -28
  32. data/lib/jsonapionify/api/resource/defaults/params.rb +3 -0
  33. data/lib/jsonapionify/api/resource/defaults/request_contexts.rb +80 -32
  34. data/lib/jsonapionify/api/resource/defaults/response_contexts.rb +13 -6
  35. data/lib/jsonapionify/api/resource/defaults.rb +1 -1
  36. data/lib/jsonapionify/api/resource/definitions/actions.rb +81 -55
  37. data/lib/jsonapionify/api/resource/definitions/attributes.rb +46 -10
  38. data/lib/jsonapionify/api/resource/definitions/contexts.rb +6 -2
  39. data/lib/jsonapionify/api/resource/definitions/helpers.rb +1 -1
  40. data/lib/jsonapionify/api/resource/definitions/pagination.rb +47 -56
  41. data/lib/jsonapionify/api/resource/definitions/params.rb +11 -15
  42. data/lib/jsonapionify/api/resource/definitions/relationships.rb +43 -7
  43. data/lib/jsonapionify/api/resource/definitions/request_headers.rb +6 -3
  44. data/lib/jsonapionify/api/resource/definitions/response_headers.rb +1 -1
  45. data/lib/jsonapionify/api/resource/definitions/scopes.rb +5 -5
  46. data/lib/jsonapionify/api/resource/definitions/sorting.rb +12 -11
  47. data/lib/jsonapionify/api/resource/definitions.rb +1 -1
  48. data/lib/jsonapionify/api/resource/error_handling.rb +92 -20
  49. data/lib/jsonapionify/api/resource/exec.rb +11 -0
  50. data/lib/jsonapionify/api/resource/includer.rb +89 -1
  51. data/lib/jsonapionify/api/resource.rb +55 -8
  52. data/lib/jsonapionify/api/response.rb +43 -14
  53. data/lib/jsonapionify/api/server/media_type.rb +36 -0
  54. data/lib/jsonapionify/api/server/request.rb +25 -11
  55. data/lib/jsonapionify/api/server.rb +8 -4
  56. data/lib/jsonapionify/api/sort_field.rb +18 -0
  57. data/lib/jsonapionify/api/sort_field_set.rb +1 -1
  58. data/lib/jsonapionify/api/test_helper.rb +46 -0
  59. data/lib/jsonapionify/documentation/template.erb +2 -2
  60. data/lib/jsonapionify/documentation.rb +10 -0
  61. data/lib/jsonapionify/structure/collections/base.rb +10 -3
  62. data/lib/jsonapionify/structure/helpers/object_defaults.rb +5 -10
  63. data/lib/jsonapionify/structure/maps/relationships.rb +4 -0
  64. data/lib/jsonapionify/structure/objects/attributes.rb +4 -0
  65. data/lib/jsonapionify/structure/objects/base.rb +22 -9
  66. data/lib/jsonapionify/structure/objects/error.rb +2 -0
  67. data/lib/jsonapionify/structure/objects/jsonapi.rb +1 -0
  68. data/lib/jsonapionify/structure/objects/link.rb +1 -0
  69. data/lib/jsonapionify/structure/objects/relationship.rb +2 -0
  70. data/lib/jsonapionify/structure/objects/resource.rb +2 -0
  71. data/lib/jsonapionify/structure/objects/resource_identifier.rb +12 -4
  72. data/lib/jsonapionify/structure/objects/top_level.rb +4 -2
  73. data/lib/jsonapionify/types/array_type.rb +16 -11
  74. data/lib/jsonapionify/types/boolean_type.rb +9 -4
  75. data/lib/jsonapionify/types/date_string_type.rb +7 -10
  76. data/lib/jsonapionify/types/float_type.rb +13 -0
  77. data/lib/jsonapionify/types/integer_type.rb +12 -0
  78. data/lib/jsonapionify/types/object_type.rb +7 -2
  79. data/lib/jsonapionify/types/string_type.rb +12 -0
  80. data/lib/jsonapionify/types/time_string_type.rb +8 -10
  81. data/lib/jsonapionify/types.rb +43 -5
  82. data/lib/jsonapionify/version.rb +1 -1
  83. data/lib/jsonapionify.rb +36 -1
  84. metadata +121 -74
checksums.yaml CHANGED
@@ -1,7 +1,15 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 515e9924931d33894090d68b518e2fe0e1509ab4
4
- data.tar.gz: eb62428441716cfc4012d90f3b3af2bb6013e158
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ NGNlYzQxYWRmZDlmZWJhODFiNDEwY2E3ZmZkNmRlNzA0YzhjODU1Zg==
5
+ data.tar.gz: !binary |-
6
+ ZGY4YjVhMWZiZGY0NmM5YWNiZGRjOGUxY2ViMmRlZTNkNTA3YjhhNw==
5
7
  SHA512:
6
- metadata.gz: eea64da6a6862c3a985fd63c8a2458fdf7c162777ca0f5116e8d20f81982fe1aafbc3023c02c4cac89b1003fe85e70e219476f53c2ebe999c8fe096c7c1ed59a
7
- data.tar.gz: c84cd475c778481ad492c7ed220b3195315e23ff8f7a1223721ce74004792273f06340b1f3738132bfc3dcd69ad715784cdd4141f43a3d65f2f68fef87eb9a8a
8
+ metadata.gz: !binary |-
9
+ ODE5ZWI5MjkxNjhhYzRjYzcwNWJlN2U1MTRlZjM4MjFiMDVlN2FhMzA3NDhh
10
+ YTc4Zjg1MzY0ZjQ3Zjg2YmUyZDdmZjQyM2ZlYzY4YmRjZjA4OTBmZjA5NDZm
11
+ YTRiZTAwOTgzNGFkYjY0YWYyZGMwNmEwZmE4YjM4OGY3NzgyY2Y=
12
+ data.tar.gz: !binary |-
13
+ YjIxZmE5M2Y4YTVkYWIzOTg1MmVlNjhhMmU5ODU5MzY4YTQ1ZmZkMDM3OTVl
14
+ MGZmZGJhM2ViYTEyYjZkZTg0YjY4OTA5ZmIyZGE2OGUyNTg5ODk3Mjk0OTJm
15
+ OWU3YmNmMTllMmFkYThmYmRjNDUyNGRjZTNlMDVhMzM0YWJiNjg=
data/.rubocop.yml CHANGED
@@ -2,6 +2,7 @@
2
2
  # This is used to easily disable Style/* checks
3
3
  AllCops:
4
4
  DisabledByDefault: true
5
+ TargetRubyVersion: 2.3
5
6
 
6
7
  ################## STYLE #################################
7
8
 
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.3.0
1
+ 2.3.1
data/.travis.yml CHANGED
@@ -6,3 +6,11 @@ cache: bundler
6
6
  addons:
7
7
  code_climate:
8
8
  repo_token: 8954c3078fac0b34ca184e61ca41e6b1dda3d820d5a0f3f18101d00bd86cdb1b
9
+ deploy:
10
+ provider: rubygems
11
+ api_key:
12
+ secure: e6pDIHcdw8vgoHQxMOyQ1AalytHueDIF4F0AaGPnLujcwQBh1WzNHpBC3ck2V9Wsk4eFZ/UaEsNvYGiZgmhPKuDC728dUAEF3Ww12a3F3/8Qgp8oiKlGQM+K7YncZesWi4/1d0R9iNTNsypuX2RuF7cUP0rMt4/RpP3AU7th6BfjYbAyR29vNxcWY51W4tYUVqQXljg32OPel05j/I9KU/KcgB0VheYPr2sb9+jzFJaU5h+bKwi/esJiC5eBXUr+JkUhl+TLiuY8lvEzmzQB9WY2WvjjMiDbsMMR642fp6a3vt4sEMj7BxcQ6+QpAyetH5xeQNsXIWiTxEroyw/F1zlBaWYNi0ees4wGCU4bi/1iN4hZ2/BN0gsn7N5ZXf8+uWILUYHjG+CT/qPvn69YbOPgW/Ap1HtZTXK5Ais5RUUJxZnAVYfKLxGLHGEg/7sEk/oCGiKlpmfnIK7wvgWU19T19Ki50wuUPAl1dZecwGHL3iK9Q7v5GcuDgIzNgseGZQHr99WzcTken9NSQd/pO1Qwli7yX3id/lmuN3qsYiEJn0DkgTRJ9Ccf9fgk32ehWt3asw1X9eXgp0Ai57LAtbCZaz+OeSFhydrBqCQ8zVe1gGAhBBr1keKyptevMyqkRmJ1z4UIxEgwIAWbvW+5e+xCDREGa5lMtQyvgcG7OwU=
13
+ gem: jsonapionify
14
+ on:
15
+ tags: true
16
+ repo: brandfolder/jsonapionify
data/README.md CHANGED
@@ -29,8 +29,91 @@ And then execute:
29
29
 
30
30
  ## Usage
31
31
 
32
- Refer to the [wiki](https://github.com/brandfolder/jsonapionify/wiki) for detailed
33
- information on how to use the framework.
32
+ ### Context: Read First
33
+
34
+ Before we get started with defining an API, you should understand a pattern used heavily throughout JSONAPIonify. This pattern is called a context. A context is a definition of data that is memoized and passed around a request. You may see context used as follows:
35
+
36
+ ```ruby
37
+ before :create do |context|
38
+ puts context.request.request_method
39
+ end
40
+ ```
41
+
42
+ Contexts can also call other contexts within their definitions:
43
+
44
+ ```ruby
45
+ context :request_method do |context|
46
+ context.request.request_method
47
+ end
48
+ ```
49
+
50
+ JSONApionify already ships with a set of predefined contexts.
51
+
52
+ ### Create an API
53
+
54
+ 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.
55
+
56
+ ```
57
+ class MyCompanyApi < JSONAPIonify::Base
58
+
59
+ # Write a description for your API.
60
+ description <<~markdown
61
+ A description of your API, that will be available at the top of the documentation.
62
+ markdown
63
+
64
+ # Add some rack middleware
65
+ use Rack::SSL::Enforcer
66
+
67
+ # Handle Authorization
68
+
69
+ end
70
+ ```
71
+
72
+ ### Predefined Contexts
73
+
74
+ #### request_body
75
+ The raw body of the request
76
+
77
+ #### request_object
78
+ The JSON parsed into a JSONApionify Structure Object. Keys can be accessed as symbols.
79
+
80
+ #### id
81
+ The id present in the request path, if present.
82
+
83
+ #### request_id
84
+ The id of the requested resource, within the data attribute of the request object.
85
+
86
+ #### request_attributes
87
+ The parsed attributes from the request object. Accessing this context, will also validate the data/structure.
88
+
89
+ #### request_relationships
90
+ The parsed relationships from the request object. Accessing this context, will also validate the data/structure.
91
+
92
+ #### request_instance
93
+ The instance of the object found from the request's data/type and data/id attibutes. This is determined from the resource's defined scope.
94
+
95
+ #### request_resource
96
+ The resource's scope determined from the request's data/type attribute.
97
+
98
+ #### request_data
99
+ The data attribute in the top level object of the request
100
+
101
+ #### authentication
102
+ An object containing the authentication data.
103
+
104
+ #### links
105
+ The links object that will be present in the response.
106
+
107
+ #### meta
108
+ The meta object that will be present in the response.
109
+
110
+ #### response_object
111
+ The jsonapi object that will be used for the response.
112
+
113
+ #### response_collection
114
+ The response for the collection.
115
+
116
+
34
117
 
35
118
  ## Development
36
119
 
@@ -42,7 +125,6 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
42
125
 
43
126
  Bug reports and pull requests are welcome on GitHub at https://github.com/brandfolder/jsonapionify. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
44
127
 
45
-
46
128
  ## License
47
129
 
48
130
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -17,6 +17,20 @@ task :missing_specs do
17
17
  end
18
18
  end
19
19
 
20
+ desc 'Open a console with jsonapionify'
21
+ task :console do
22
+ require 'jsonapionify'
23
+ Pry.start
24
+ end
25
+
26
+ task :benchmark do
27
+ require 'jsonapionify'
28
+ toplevel = nil
29
+ puts parse: Benchmark.realtime { toplevel = JSONAPIonify.parse(File.read('spec/fixtures/sample.json')) }
30
+ puts validate: Benchmark.realtime { toplevel.validate }
31
+ puts generate: Benchmark.realtime { toplevel.to_json(validate: false) }
32
+ end
33
+
20
34
  desc 'Remove empty specs'
21
35
  task :prune_specs do
22
36
  empty_specs = Dir.glob("./spec/**/*_spec.rb").select do |f|
data/jsonapionify.gemspec CHANGED
@@ -31,6 +31,9 @@ Gem::Specification.new do |spec|
31
31
  spec.add_dependency 'possessive'
32
32
  spec.add_dependency 'unstrict_proc'
33
33
  spec.add_dependency 'enumerable_observer'
34
+ spec.add_dependency 'concurrent-ruby', '~> 1.0.1'
35
+ spec.add_dependency 'concurrent-ruby-ext'
36
+ spec.add_dependency 'mime-types'
34
37
 
35
38
  spec.add_development_dependency 'pry'
36
39
  spec.add_development_dependency 'rocco'
@@ -1,7 +1,10 @@
1
+ require 'unstrict_proc'
2
+
1
3
  module JSONAPIonify::Api
2
4
  class Action
3
- attr_reader :name, :request_block, :content_type, :responses, :prepend,
4
- :path, :request_method, :only_associated
5
+ attr_reader :name, :block, :content_type, :responses, :prepend,
6
+ :path, :request_method, :only_associated, :cacheable,
7
+ :callbacks
5
8
 
6
9
  def self.dummy(&block)
7
10
  new(nil, nil, &block)
@@ -13,17 +16,25 @@ module JSONAPIonify::Api
13
16
  end
14
17
  end
15
18
 
16
- def initialize(name, request_method, path = nil, require_body = nil, example_type = :resource, content_type: nil, prepend: nil, only_associated: false, &block)
19
+ def initialize(name, request_method, path = nil,
20
+ example_input: nil,
21
+ content_type: nil,
22
+ prepend: nil,
23
+ only_associated: false,
24
+ cacheable: false,
25
+ callbacks: true,
26
+ &block)
17
27
  @request_method = request_method
18
- @require_body = require_body.nil? ? %w{POST PUT PATCH}.include?(@request_method) : require_body
19
28
  @path = path || ''
20
29
  @prepend = prepend
21
30
  @only_associated = only_associated
22
31
  @name = name
23
- @example_type = example_type
32
+ @example_input = example_input
24
33
  @content_type = content_type || 'application/vnd.api+json'
25
- @request_block = block || proc {}
34
+ @block = block || proc {}
26
35
  @responses = []
36
+ @cacheable = cacheable
37
+ @callbacks = callbacks
27
38
  end
28
39
 
29
40
  def initialize_copy(new_instance)
@@ -45,8 +56,15 @@ module JSONAPIonify::Api
45
56
  end
46
57
 
47
58
  def path_regex(base, name, include_path)
48
- raw_reqexp = build_path(base, name, include_path).gsub(':id', '(?<id>[^\/]+)')
49
- Regexp.new('^' + raw_reqexp + '$')
59
+ raw_reqexp =
60
+ build_path(
61
+ base, name, include_path
62
+ ).gsub(
63
+ ':id', '(?<id>[^\/]+)'
64
+ ).gsub(
65
+ '/*', '/?[^\/]*'
66
+ )
67
+ Regexp.new('^' + raw_reqexp + '(\.[A-Za-z_-]+)?$')
50
68
  end
51
69
 
52
70
  def ==(other)
@@ -70,20 +88,43 @@ module JSONAPIonify::Api
70
88
  )
71
89
  end
72
90
 
91
+ def example_input(resource)
92
+ request = Server::Request.env_for('http://example.org', request_method)
93
+ context = ContextDelegate::Mock.new(
94
+ request: request, resource: resource.new, _is_example_: true, includes: []
95
+ )
96
+ case @example_input
97
+ when :resource
98
+ {
99
+ 'data' => resource.build_resource(
100
+ context,
101
+ resource.example_instance_for_action(name, context),
102
+ relationships: false,
103
+ links: false,
104
+ fields: resource.fields_for_action(name, context)
105
+ ).as_json
106
+ }.to_json
107
+ when :resource_identifier
108
+ {
109
+ 'data' => resource.build_resource_identifier(
110
+ resource.example_instance_for_action(name, context)
111
+ ).as_json
112
+ }.to_json
113
+ when Proc
114
+ @example_input.call
115
+ end
116
+ end
117
+
73
118
  def example_requests(resource, url)
74
119
  responses.map do |response|
75
- opts = {}
76
- opts['HTTP_CONTENT_TYPE'] = content_type if @require_body
77
- opts['HTTP_ACCEPT'] = response.accept
78
- request = Server::Request.env_for(url, request_method, opts)
79
- opts[:input] = case @example_type
80
- when :resource
81
- { 'data' => resource.build_resource(request, resource.example_instance, relationships: false, links: false).as_json }.to_json
82
- when :resource_identifier
83
- { 'data' => resource.build_resource_identifier(resource.example_instance).as_json }.to_json
84
- end if @content_type == 'application/vnd.api+json' && !%w{GET DELETE}.include?(request_method)
85
- request = Server::Request.env_for(url, request_method, opts)
86
- response = Server::MockResponse.new(*sample_request(resource, request))
120
+ opts = {}
121
+ opts['CONTENT_TYPE'] = content_type if @example_input
122
+ opts['HTTP_ACCEPT'] = response.accept
123
+ if content_type == 'application/vnd.api+json' && @example_input
124
+ opts[:input] = example_input(resource)
125
+ end
126
+ request = Server::Request.env_for(url, request_method, opts)
127
+ response = Server::MockResponse.new(*sample_request(resource, request))
87
128
 
88
129
  OpenStruct.new(
89
130
  request: request.http_string,
@@ -106,116 +147,38 @@ module JSONAPIonify::Api
106
147
  supports_content_type?(request)
107
148
  end
108
149
 
109
- def response(status: nil, accept: nil, &block)
110
- new_response = Response.new(self, status: status, accept: accept, &block)
150
+ def response(**options, &block)
151
+ new_response = Response.new(self, **options, &block)
111
152
  @responses.delete new_response
112
- @responses << new_response
153
+ @responses.push new_response
113
154
  self
114
155
  end
115
156
 
116
- def sample_request(resource, request)
117
- action = dup
118
- resource.new.instance_eval do
119
- sample_context = self.class.context_definitions.dup
120
- sample_context[:collection] =
121
- Context.new proc { 3.times.map.each_with_index { |i| resource.example_instance(i + 1) } }, true
122
- sample_context[:paginated_collection] = Context.new proc { |context| context.collection }
123
- sample_context[:instance] = Context.new proc { |context| context.collection.first }
124
- if sample_context.has_key? :owner_context
125
- sample_context[:owner_context] = Context.new proc { ContextDelegate::Mock.new }, true
126
- end
127
-
128
- # Bootstrap the Action
129
- context = ContextDelegate.new(request, self, sample_context)
130
-
131
- define_singleton_method :response_headers do
132
- context.response_headers
157
+ def sample_context(resource)
158
+ resource.context_definitions.dup.tap do |defs|
159
+ collection_context = proc do |context|
160
+ 3.times.map { resource.example_instance_for_action(action.name, context) }
133
161
  end
134
-
135
- # Render the response
136
- response_definition =
137
- action.responses.find { |response| response.accept? request } ||
138
- error_now(:not_acceptable)
139
- response_definition.call(self, context)
162
+ defs[:_is_example_] = Context.new(readonly: true) { true }
163
+ defs[:collection] = Context.new(&collection_context)
164
+ defs[:paginated_collection] = Context.new { |context| context.collection }
165
+ defs[:instance] = Context.new(readonly: true) { |context| context.collection.first }
166
+ defs[:owner_context] = Context.new(readonly: true) { ContextDelegate::Mock.new } if defs.has_key? :owner_context
140
167
  end
141
168
  end
142
169
 
143
- def call(resource, request)
144
- action = dup
145
- cache_options = {}
146
- resource.new.instance_eval do
147
- # Bootstrap the Action
148
- context = ContextDelegate.new(request, self, self.class.context_definitions)
149
-
150
- # Define Singletons
151
- define_singleton_method :cache do |key, **options|
152
- raise Errors::DoubleCacheError, "Cache was already called for this action" if @called
153
- @called = true
154
- cache_options.merge! options
155
-
156
- # Build the cache key, and obscure it.
157
- context.meta[:cache_key] =
158
- cache_options[:key] =
159
- Base64.urlsafe_encode64(
160
- {
161
- dsl: JSONAPIonify.digest,
162
- api: self.class.api.signature,
163
- path: request.path,
164
- accept: request.accept,
165
- params: context.params,
166
- key: key,
167
- }.to_json
168
- )
169
- # If the cache exists, then fail to cache miss
170
- if self.class.cache_store.exist?(cache_options[:key])
171
- raise Errors::CacheHit, cache_options[:key]
172
- end
173
- end if request.get?
174
-
175
- define_singleton_method :action_name do
176
- action.name
177
- end
178
-
179
- define_singleton_method :errors do
180
- context.errors
181
- end
182
-
183
- define_singleton_method :response_headers do
184
- context.response_headers
185
- end
170
+ def sample_request(resource, request)
171
+ call(resource, request, context_definitions: sample_context(resource), callbacks: false)
172
+ end
186
173
 
187
- begin
188
- # Run Callbacks
189
- [:request, action.name].each do |callback|
190
- case run_callbacks(callback, context) { errors.present? }
191
- when true # Boolean true means errors
192
- raise Errors::RequestError
193
- when nil # nil means no result, callback failed
194
- error_now :internal_server_error
195
- end if action.name
196
- end
197
-
198
- # Start the request
199
- instance_exec(context, &action.request_block)
200
- fail Errors::RequestError if errors.present?
201
- response_definition =
202
- action.responses.find { |response| response.accept? request } ||
203
- error_now(:not_acceptable)
204
- response_definition.call(self, context).tap do |status, headers, body|
205
- self.class.cache_store.write(
206
- cache_options[:key],
207
- [status, headers, body.body],
208
- **cache_options.except(:key)
209
- ) if request.get? && cache_options.present?
210
- end
211
- rescue Errors::RequestError
212
- error_response
213
- rescue Errors::CacheHit
214
- self.class.cache_store.read cache_options[:key]
215
- rescue Exception => exception
216
- rescued_response exception
217
- end
218
- end
174
+ def call(resource, request, callbacks: self.callbacks, **opts)
175
+ resource.new(
176
+ request: request,
177
+ callbacks: callbacks,
178
+ cacheable: cacheable,
179
+ action: self,
180
+ **opts
181
+ ).call
219
182
  end
220
183
  end
221
184
  end
@@ -3,20 +3,34 @@ require 'unstrict_proc'
3
3
  module JSONAPIonify::Api
4
4
  class Attribute
5
5
  using UnstrictProc
6
- attr_reader :name, :type, :description, :read, :write, :required
6
+ attr_reader :name, :type, :description, :read, :write, :required, :block
7
7
 
8
- def initialize(name, type, description, read: true, write: true, required: false, example: nil)
9
- raise ArgumentError, 'required attributes must be writable' if required && !write
8
+ def initialize(
9
+ name,
10
+ type,
11
+ description,
12
+ read: true,
13
+ write: true,
14
+ required: false,
15
+ example: nil,
16
+ &block
17
+ )
10
18
  unless type.is_a? JSONAPIonify::Types::BaseType
11
19
  raise TypeError, "#{type} is not a valid JSON type"
12
20
  end
13
- @name = name
14
- @type = type
15
- @description = description
16
- @example = example
17
- @read = read
18
- @write = write
19
- @required = write ? required : false
21
+
22
+ @name = name.to_sym
23
+ @type = type&.freeze
24
+ @description = description&.freeze
25
+ @example = example&.freeze
26
+ @read = read&.freeze
27
+ @write = write&.freeze
28
+ @required = required&.freeze
29
+ @block = block&.freeze
30
+ @writeable_actions = write
31
+ @readable_actions = read
32
+
33
+ freeze
20
34
  end
21
35
 
22
36
  def ==(other)
@@ -24,19 +38,82 @@ module JSONAPIonify::Api
24
38
  self.name == other.name
25
39
  end
26
40
 
27
- def options_json
28
- {
29
- name: name,
30
- required: required
31
- }
41
+ def supports_read_for_action?(action_name, context)
42
+ case (setting = @readable_actions)
43
+ when TrueClass, FalseClass
44
+ setting
45
+ when Hash
46
+ !!JSONAPIonify::Continuation.new(setting).check(action_name, context) { true }
47
+ when Array
48
+ setting.map(&:to_sym).include? action_name
49
+ when Symbol, String
50
+ setting.to_sym === action_name
51
+ else
52
+ false
53
+ end
54
+ end
55
+
56
+ def supports_write_for_action?(action_name, context)
57
+ action = context.resource.class.actions.find { |a| a.name == action_name }
58
+ return false unless %{POST PUT PATCH}.include? action.request_method
59
+ case (setting = @writeable_actions)
60
+ when TrueClass, FalseClass
61
+ setting
62
+ when Hash
63
+ !!JSONAPIonify::Continuation.new(setting).check(action_name, context) { true }
64
+ when Array
65
+ setting.map(&:to_sym).include? action_name
66
+ when Symbol, String
67
+ setting.to_sym === action_name
68
+ else
69
+ false
70
+ end
71
+ end
72
+
73
+ def resolve(instance, context, example_id: nil)
74
+ if context.respond_to?(:_is_example_) && context._is_example_ == true
75
+ return example(example_id)
76
+ end
77
+ block = self.block || proc { |attr, i| i.send attr }
78
+ type.dump block.unstrict.call(self.name, instance, context)
79
+ rescue JSONAPIonify::Types::DumpError => ex
80
+ error_block =
81
+ context.resource.class.error_definitions[:attribute_type_error]
82
+ context.errors.evaluate(
83
+ name,
84
+ error_block: error_block,
85
+ backtrace: ex.backtrace,
86
+ runtime_block: proc {
87
+ detail ex.message
88
+ }
89
+ )
90
+ rescue JSONAPIonify::Types::NotNullError => ex
91
+ error_block =
92
+ context.resource.class.error_definitions[:attribute_cannot_be_null]
93
+ context.errors.evaluate(
94
+ name,
95
+ error_block: error_block,
96
+ backtrace: ex.backtrace,
97
+ runtime_block: proc {}
98
+ )
99
+ nil
32
100
  end
33
101
 
34
- def required?
35
- !!@required
102
+ def required_for_action?(action_name, context)
103
+ supports_write_for_action?(action_name, context) &&
104
+ (required === true || Array.wrap(required).include?(action_name))
36
105
  end
37
106
 
38
- def optional?
39
- !required?
107
+ def options_json_for_action(action_name, context)
108
+ {
109
+ name: @name,
110
+ type: @type.to_s,
111
+ description: JSONAPIonify::Documentation.onelinify_markdown(description),
112
+ example: example(context.resource.class.generate_id)
113
+ }.tap do |opts|
114
+ opts[:not_null] = true if @type.not_null?
115
+ opts[:required] = true if required_for_action?(action_name, context)
116
+ end
40
117
  end
41
118
 
42
119
  def read?
@@ -62,7 +139,7 @@ module JSONAPIonify::Api
62
139
  OpenStruct.new(
63
140
  name: name,
64
141
  type: type.name,
65
- required: required?,
142
+ required: Array.wrap(required).join(', '),
66
143
  description: JSONAPIonify::Documentation.render_markdown(description),
67
144
  allow: allow
68
145
  )
@@ -21,7 +21,8 @@ module JSONAPIonify::Api
21
21
  end
22
22
 
23
23
  def load_resources
24
- return unless load_path || resources_loaded?
24
+ return if !load_path || resources_loaded?
25
+ superclass.load_resources if superclass.respond_to? :load_resources
25
26
  @documentation_output = nil
26
27
  @last_signature = resource_signature
27
28
  $".delete_if { |s| s.start_with? load_path }
@@ -34,8 +35,8 @@ module JSONAPIonify::Api
34
35
  @last_signature == resource_signature
35
36
  end
36
37
 
37
- def http_error(action, request)
38
- Action.error(action).call(resource_class, request)
38
+ def http_error(error, request, &block)
39
+ Action.error(error, &block).call(resource_class, request)
39
40
  end
40
41
 
41
42
  def root_url(request)
@@ -46,7 +47,7 @@ module JSONAPIonify::Api
46
47
  end
47
48
 
48
49
  def process_index(request)
49
- headers = ContextDelegate.new(request, resource_class.new, resource_class.context_definitions).response_headers
50
+ headers = resource_class.new(request: request).exec { |c| c.response_headers }
50
51
  obj = JSONAPIonify.new_object
51
52
  obj[:meta] = { resources: {} }
52
53
  obj[:links] = { self: request.url }
@@ -4,11 +4,27 @@ module JSONAPIonify::Api
4
4
  def self.extended(klass)
5
5
  klass.class_eval do
6
6
  class << self
7
- delegate :context, :response_header, :helper, :rescue_from, :error,
8
- :enable_pagination, :before, :param, :request_header,
9
- :define_pagination_strategy, :define_sorting_strategy,
10
- :sticky_params, :authentication, :on_exception,
7
+ delegate :context,
8
+ :response_header,
9
+ :helper,
10
+ :rescue_from,
11
+ :register_exception,
12
+ :error,
13
+ :enable_pagination,
14
+ :before,
15
+ :param,
16
+ :request_header,
17
+ :define_pagination_strategy,
18
+ :define_sorting_strategy,
19
+ :sticky_params,
20
+ :authentication,
21
+ :on_exception,
11
22
  :example_id_generator,
23
+ :after,
24
+ :builder,
25
+ :types,
26
+ :attribute,
27
+
12
28
  to: :resource_class
13
29
  end
14
30
  end
@@ -26,9 +26,9 @@ module JSONAPIonify::Api
26
26
  end
27
27
 
28
28
  def documentation_output(request)
29
- #cache_store.fetch(resource_signature) do
30
- JSONAPIonify::Documentation.new(documentation_object(request)).result
31
- #end
29
+ cache_store.fetch(resource_signature) do
30
+ JSONAPIonify::Documentation.new(documentation_object(request)).result
31
+ end
32
32
  end
33
33
 
34
34
  def resources_in_order
@@ -2,7 +2,7 @@ module JSONAPIonify::Api
2
2
  class Base::Reloader < Struct.new :app
3
3
 
4
4
  def call(env)
5
- Base.descendants.each(&:load_resources)
5
+ Base.descendants.map(&:load_resources)
6
6
  app.call(env)
7
7
  end
8
8