jsonapionify 0.9.0 → 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +13 -5
- data/.rubocop.yml +1 -0
- data/.ruby-version +1 -1
- data/.travis.yml +8 -0
- data/README.md +85 -3
- data/Rakefile +14 -0
- data/jsonapionify.gemspec +3 -0
- data/lib/jsonapionify/api/action.rb +84 -121
- data/lib/jsonapionify/api/attribute.rb +97 -20
- data/lib/jsonapionify/api/base/class_methods.rb +5 -4
- data/lib/jsonapionify/api/base/delegation.rb +20 -4
- data/lib/jsonapionify/api/base/doc_helper.rb +3 -3
- data/lib/jsonapionify/api/base/reloader.rb +1 -1
- data/lib/jsonapionify/api/base/resource_definitions.rb +28 -15
- data/lib/jsonapionify/api/base.rb +6 -0
- data/lib/jsonapionify/api/context.rb +18 -5
- data/lib/jsonapionify/api/context_delegate.rb +24 -7
- data/lib/jsonapionify/api/errors.rb +2 -0
- data/lib/jsonapionify/api/errors_object.rb +6 -5
- data/lib/jsonapionify/api/relationship/blocks.rb +1 -1
- data/lib/jsonapionify/api/relationship/many.rb +35 -11
- data/lib/jsonapionify/api/relationship/one.rb +17 -7
- data/lib/jsonapionify/api/relationship.rb +20 -6
- data/lib/jsonapionify/api/resource/builders.rb +81 -30
- data/lib/jsonapionify/api/resource/caching.rb +28 -0
- data/lib/jsonapionify/api/resource/caller.rb +61 -0
- data/lib/jsonapionify/api/resource/class_methods.rb +6 -2
- data/lib/jsonapionify/api/resource/defaults/actions.rb +47 -0
- data/lib/jsonapionify/api/resource/defaults/errors.rb +61 -15
- data/lib/jsonapionify/api/resource/defaults/hooks.rb +68 -0
- data/lib/jsonapionify/api/resource/defaults/options.rb +16 -28
- data/lib/jsonapionify/api/resource/defaults/params.rb +3 -0
- data/lib/jsonapionify/api/resource/defaults/request_contexts.rb +80 -32
- data/lib/jsonapionify/api/resource/defaults/response_contexts.rb +13 -6
- data/lib/jsonapionify/api/resource/defaults.rb +1 -1
- data/lib/jsonapionify/api/resource/definitions/actions.rb +81 -55
- data/lib/jsonapionify/api/resource/definitions/attributes.rb +46 -10
- data/lib/jsonapionify/api/resource/definitions/contexts.rb +6 -2
- data/lib/jsonapionify/api/resource/definitions/helpers.rb +1 -1
- data/lib/jsonapionify/api/resource/definitions/pagination.rb +47 -56
- data/lib/jsonapionify/api/resource/definitions/params.rb +11 -15
- data/lib/jsonapionify/api/resource/definitions/relationships.rb +43 -7
- data/lib/jsonapionify/api/resource/definitions/request_headers.rb +6 -3
- data/lib/jsonapionify/api/resource/definitions/response_headers.rb +1 -1
- data/lib/jsonapionify/api/resource/definitions/scopes.rb +5 -5
- data/lib/jsonapionify/api/resource/definitions/sorting.rb +12 -11
- data/lib/jsonapionify/api/resource/definitions.rb +1 -1
- data/lib/jsonapionify/api/resource/error_handling.rb +92 -20
- data/lib/jsonapionify/api/resource/exec.rb +11 -0
- data/lib/jsonapionify/api/resource/includer.rb +89 -1
- data/lib/jsonapionify/api/resource.rb +55 -8
- data/lib/jsonapionify/api/response.rb +43 -14
- data/lib/jsonapionify/api/server/media_type.rb +36 -0
- data/lib/jsonapionify/api/server/request.rb +25 -11
- data/lib/jsonapionify/api/server.rb +8 -4
- data/lib/jsonapionify/api/sort_field.rb +18 -0
- data/lib/jsonapionify/api/sort_field_set.rb +1 -1
- data/lib/jsonapionify/api/test_helper.rb +46 -0
- data/lib/jsonapionify/documentation/template.erb +2 -2
- data/lib/jsonapionify/documentation.rb +10 -0
- data/lib/jsonapionify/structure/collections/base.rb +10 -3
- data/lib/jsonapionify/structure/helpers/object_defaults.rb +5 -10
- data/lib/jsonapionify/structure/maps/relationships.rb +4 -0
- data/lib/jsonapionify/structure/objects/attributes.rb +4 -0
- data/lib/jsonapionify/structure/objects/base.rb +22 -9
- data/lib/jsonapionify/structure/objects/error.rb +2 -0
- data/lib/jsonapionify/structure/objects/jsonapi.rb +1 -0
- data/lib/jsonapionify/structure/objects/link.rb +1 -0
- data/lib/jsonapionify/structure/objects/relationship.rb +2 -0
- data/lib/jsonapionify/structure/objects/resource.rb +2 -0
- data/lib/jsonapionify/structure/objects/resource_identifier.rb +12 -4
- data/lib/jsonapionify/structure/objects/top_level.rb +4 -2
- data/lib/jsonapionify/types/array_type.rb +16 -11
- data/lib/jsonapionify/types/boolean_type.rb +9 -4
- data/lib/jsonapionify/types/date_string_type.rb +7 -10
- data/lib/jsonapionify/types/float_type.rb +13 -0
- data/lib/jsonapionify/types/integer_type.rb +12 -0
- data/lib/jsonapionify/types/object_type.rb +7 -2
- data/lib/jsonapionify/types/string_type.rb +12 -0
- data/lib/jsonapionify/types/time_string_type.rb +8 -10
- data/lib/jsonapionify/types.rb +43 -5
- data/lib/jsonapionify/version.rb +1 -1
- data/lib/jsonapionify.rb +36 -1
- metadata +121 -74
checksums.yaml
CHANGED
@@ -1,7 +1,15 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NGNlYzQxYWRmZDlmZWJhODFiNDEwY2E3ZmZkNmRlNzA0YzhjODU1Zg==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ZGY4YjVhMWZiZGY0NmM5YWNiZGRjOGUxY2ViMmRlZTNkNTA3YjhhNw==
|
5
7
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
|
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
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.3.
|
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
|
-
|
33
|
-
|
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, :
|
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,
|
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
|
-
@
|
32
|
+
@example_input = example_input
|
24
33
|
@content_type = content_type || 'application/vnd.api+json'
|
25
|
-
@
|
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 =
|
49
|
-
|
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['
|
77
|
-
opts['HTTP_ACCEPT']
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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(
|
110
|
-
new_response = Response.new(self,
|
150
|
+
def response(**options, &block)
|
151
|
+
new_response = Response.new(self, **options, &block)
|
111
152
|
@responses.delete new_response
|
112
|
-
@responses
|
153
|
+
@responses.push new_response
|
113
154
|
self
|
114
155
|
end
|
115
156
|
|
116
|
-
def
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
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
|
144
|
-
|
145
|
-
|
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
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
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(
|
9
|
-
|
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
|
-
|
14
|
-
@
|
15
|
-
@
|
16
|
-
@
|
17
|
-
@
|
18
|
-
@
|
19
|
-
@
|
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
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
35
|
-
|
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
|
39
|
-
|
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
|
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(
|
38
|
-
Action.error(
|
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 =
|
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,
|
8
|
-
:
|
9
|
-
:
|
10
|
-
:
|
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
|
-
|
30
|
-
|
31
|
-
|
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
|