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.
- 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
|