jsonapionify 0.9.0 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
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