jsonapionify 0.0.1.pre → 0.9.0
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 +4 -4
- data/.editorconfig +35 -0
- data/.ruby-version +1 -1
- data/.travis.yml +0 -2
- data/Guardfile +1 -1
- data/README.md +13 -8
- data/Rakefile +10 -0
- data/config.ru +3 -3
- data/index.html +1 -0
- data/jsonapionify.gemspec +13 -8
- data/lib/jsonapionify/api/action.rb +60 -50
- data/lib/jsonapionify/api/attribute.rb +13 -2
- data/lib/jsonapionify/api/base/app_builder.rb +17 -2
- data/lib/jsonapionify/api/base/class_methods.rb +33 -17
- data/lib/jsonapionify/api/base/delegation.rb +4 -1
- data/lib/jsonapionify/api/base/doc_helper.rb +13 -4
- data/lib/jsonapionify/api/base/resource_definitions.rb +13 -2
- data/lib/jsonapionify/api/base.rb +22 -6
- data/lib/jsonapionify/api/context_delegate.rb +2 -2
- data/lib/jsonapionify/api/errors.rb +7 -2
- data/lib/jsonapionify/api/errors_object.rb +1 -1
- data/lib/jsonapionify/api/header_options.rb +6 -5
- data/lib/jsonapionify/api/param_options.rb +49 -7
- data/lib/jsonapionify/api/relationship/many.rb +0 -5
- data/lib/jsonapionify/api/relationship/one.rb +10 -9
- data/lib/jsonapionify/api/relationship.rb +17 -5
- data/lib/jsonapionify/api/resource/builders.rb +39 -10
- data/lib/jsonapionify/api/resource/class_methods.rb +17 -6
- data/lib/jsonapionify/api/resource/defaults/actions.rb +0 -1
- data/lib/jsonapionify/api/resource/defaults/errors.rb +11 -11
- data/lib/jsonapionify/api/resource/defaults/options.rb +53 -0
- data/lib/jsonapionify/api/resource/defaults/params.rb +9 -0
- data/lib/jsonapionify/api/resource/defaults/request_contexts.rb +17 -11
- data/lib/jsonapionify/api/resource/definitions/actions.rb +51 -45
- data/lib/jsonapionify/api/resource/definitions/attributes.rb +2 -2
- data/lib/jsonapionify/api/resource/definitions/helpers.rb +18 -0
- data/lib/jsonapionify/api/resource/definitions/pagination.rb +183 -53
- data/lib/jsonapionify/api/resource/definitions/params.rb +43 -12
- data/lib/jsonapionify/api/resource/definitions/request_headers.rb +1 -67
- data/lib/jsonapionify/api/resource/definitions/scopes.rb +2 -13
- data/lib/jsonapionify/api/resource/definitions/sorting.rb +71 -58
- data/lib/jsonapionify/api/resource/error_handling.rb +2 -2
- data/lib/jsonapionify/api/resource/includer.rb +6 -0
- data/lib/jsonapionify/api/resource.rb +14 -3
- data/lib/jsonapionify/api/response.rb +2 -2
- data/lib/jsonapionify/api/server/mock_response.rb +2 -2
- data/lib/jsonapionify/api/server/request.rb +11 -7
- data/lib/jsonapionify/api/server.rb +1 -1
- data/lib/jsonapionify/api/sort_field.rb +59 -0
- data/lib/jsonapionify/api/sort_field_set.rb +36 -0
- data/lib/jsonapionify/callbacks.rb +3 -3
- data/lib/jsonapionify/continuation.rb +1 -0
- data/lib/jsonapionify/deep_sort_collection.rb +22 -0
- data/lib/jsonapionify/documentation/template.erb +196 -77
- data/lib/jsonapionify/documentation.rb +9 -9
- data/lib/jsonapionify/indented_string.rb +1 -0
- data/lib/jsonapionify/inherited_attributes.rb +4 -3
- data/lib/jsonapionify/structure/collections/base.rb +2 -1
- data/lib/jsonapionify/structure/helpers/errors.rb +1 -1
- data/lib/jsonapionify/structure/helpers/object_defaults.rb +2 -1
- data/lib/jsonapionify/structure/helpers/validations.rb +2 -1
- data/lib/jsonapionify/structure/objects/base.rb +4 -3
- data/lib/jsonapionify/structure/objects/top_level.rb +1 -1
- data/lib/jsonapionify/types/boolean_type.rb +2 -2
- data/lib/jsonapionify/types/date_string_type.rb +1 -1
- data/lib/jsonapionify/types/time_string_type.rb +1 -1
- data/lib/jsonapionify/version.rb +1 -1
- data/lib/jsonapionify.rb +16 -2
- metadata +69 -10
- data/fixtures/documentation.json +0 -364
- data/lib/jsonapionify/api/resource/http.rb +0 -11
- data/lib/jsonapionify/enumerable_observer.rb +0 -91
- data/lib/jsonapionify/unstrict_proc.rb +0 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 515e9924931d33894090d68b518e2fe0e1509ab4
|
4
|
+
data.tar.gz: eb62428441716cfc4012d90f3b3af2bb6013e158
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eea64da6a6862c3a985fd63c8a2458fdf7c162777ca0f5116e8d20f81982fe1aafbc3023c02c4cac89b1003fe85e70e219476f53c2ebe999c8fe096c7c1ed59a
|
7
|
+
data.tar.gz: c84cd475c778481ad492c7ed220b3195315e23ff8f7a1223721ce74004792273f06340b1f3738132bfc3dcd69ad715784cdd4141f43a3d65f2f68fef87eb9a8a
|
data/.editorconfig
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
root = true
|
2
|
+
|
3
|
+
[*]
|
4
|
+
insert_final_newline = true
|
5
|
+
trim_trailing_whitespace = true
|
6
|
+
charset = utf-8
|
7
|
+
|
8
|
+
# Ruby Files
|
9
|
+
[{*.rb, Gemfile*, config.ru, Rakefile, *.rake}]
|
10
|
+
indent_style = space
|
11
|
+
indent_size = 2
|
12
|
+
|
13
|
+
# SQL
|
14
|
+
[*.sql]
|
15
|
+
indent_style = space
|
16
|
+
indent_size = 4
|
17
|
+
|
18
|
+
# Makefiles
|
19
|
+
[{Makefile, *.mk}]
|
20
|
+
indent_style = tab
|
21
|
+
indent_size = 1
|
22
|
+
|
23
|
+
# ERB
|
24
|
+
[*.erb]
|
25
|
+
indent_style = space
|
26
|
+
indent_size = 2
|
27
|
+
|
28
|
+
# Yaml
|
29
|
+
[*.yml]
|
30
|
+
indent_style = space
|
31
|
+
indent_size = 2
|
32
|
+
|
33
|
+
# Markdown uses trailing whitespace for linebreaks.
|
34
|
+
[{*.markdown, *.md}]
|
35
|
+
trim_trailing_whitespace = false
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.3.0
|
data/.travis.yml
CHANGED
data/Guardfile
CHANGED
data/README.md
CHANGED
@@ -1,10 +1,19 @@
|
|
1
1
|
# JSONAPIonify
|
2
|
-
[](https://badge.fury.io/rb/jsonapionify)
|
3
3
|
[](https://travis-ci.org/brandfolder/jsonapionify)
|
4
4
|
[](https://codeclimate.com/repos/5672446f137f95309c0067c6/feed)
|
5
5
|
[](https://codeclimate.com/repos/5672446f137f95309c0067c6/coverage)
|
6
6
|
|
7
|
-
|
7
|
+
JSONAPIonify is a framework for building JSONApi 1.0 compliant
|
8
|
+
APIs. It can run as a standalone rack app or as part of a larger framework such
|
9
|
+
as rails. In addition, it auto-generates beautiful documentation.
|
10
|
+
|
11
|
+
Live Example:
|
12
|
+
|
13
|
+
* [Resource](https://api.brandfolder.com/v2/slug/brandfolder)
|
14
|
+
* [Documentation](https://api.brandfolder.com/v2/docs) (screenshot below)
|
15
|
+
|
16
|
+
[](https://api.brandfolder.com/v2/docs)
|
8
17
|
|
9
18
|
## Installation
|
10
19
|
|
@@ -18,13 +27,10 @@ And then execute:
|
|
18
27
|
|
19
28
|
$ bundle
|
20
29
|
|
21
|
-
Or install it yourself as:
|
22
|
-
|
23
|
-
$ gem install jsonapionify
|
24
|
-
|
25
30
|
## Usage
|
26
31
|
|
27
|
-
|
32
|
+
Refer to the [wiki](https://github.com/brandfolder/jsonapionify/wiki) for detailed
|
33
|
+
information on how to use the framework.
|
28
34
|
|
29
35
|
## Development
|
30
36
|
|
@@ -40,4 +46,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/brandf
|
|
40
46
|
## License
|
41
47
|
|
42
48
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
43
|
-
|
data/Rakefile
CHANGED
@@ -17,6 +17,16 @@ task :missing_specs do
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
+
desc 'Remove empty specs'
|
21
|
+
task :prune_specs do
|
22
|
+
empty_specs = Dir.glob("./spec/**/*_spec.rb").select do |f|
|
23
|
+
File.read(f).empty?
|
24
|
+
end
|
25
|
+
empty_specs.each do |f|
|
26
|
+
FileUtils.rm f
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
20
30
|
STATS_DIRECTORIES = [
|
21
31
|
%w(Structure lib/jsonapionify/structure),
|
22
32
|
%w(Server lib/jsonapionify/api),
|
data/config.ru
CHANGED
@@ -4,12 +4,12 @@ require 'jsonapionify'
|
|
4
4
|
require 'navigable_hash'
|
5
5
|
require 'json'
|
6
6
|
|
7
|
-
api
|
8
|
-
docs
|
7
|
+
api = NavigableHash.new JSON.load File.read 'fixtures/documentation.json'
|
8
|
+
docs = JSONAPIonify::Documentation.new(api)
|
9
9
|
result = docs.result
|
10
10
|
|
11
11
|
run ->(_) {
|
12
12
|
response = Rack::Response.new
|
13
13
|
response.write result
|
14
14
|
response.finish
|
15
|
-
}
|
15
|
+
}
|
data/index.html
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
My Page
|
data/jsonapionify.gemspec
CHANGED
@@ -4,21 +4,22 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'jsonapionify/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name
|
8
|
-
spec.version
|
9
|
-
spec.authors
|
10
|
-
spec.email
|
7
|
+
spec.name = "jsonapionify"
|
8
|
+
spec.version = JSONAPIonify::VERSION
|
9
|
+
spec.authors = ["Jason Waldrip"]
|
10
|
+
spec.email = ["jason@waldrip.net"]
|
11
11
|
|
12
|
-
spec.summary
|
13
|
-
spec.homepage
|
14
|
-
spec.license
|
12
|
+
spec.summary = %q{Ruby object structure conforming to the JSON API spec.}
|
13
|
+
spec.homepage = "https://github.com/brandfolder/jsonapionify"
|
14
|
+
spec.license = "MIT"
|
15
15
|
|
16
16
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(vendor|spec)/}) }
|
17
17
|
spec.bindir = "exe"
|
18
18
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
-
|
21
|
+
version_file = File.expand_path '../.ruby-version', __FILE__
|
22
|
+
spec.required_ruby_version = "~> #{File.read(version_file).strip.sub(/\-p[0-9]+$/, '')}"
|
22
23
|
|
23
24
|
spec.add_dependency "activesupport", "~> 4.2"
|
24
25
|
spec.add_dependency "faraday", "~> 0.9"
|
@@ -27,6 +28,9 @@ Gem::Specification.new do |spec|
|
|
27
28
|
spec.add_dependency "oj"
|
28
29
|
spec.add_dependency "rack-test"
|
29
30
|
spec.add_dependency 'faker'
|
31
|
+
spec.add_dependency 'possessive'
|
32
|
+
spec.add_dependency 'unstrict_proc'
|
33
|
+
spec.add_dependency 'enumerable_observer'
|
30
34
|
|
31
35
|
spec.add_development_dependency 'pry'
|
32
36
|
spec.add_development_dependency 'rocco'
|
@@ -47,4 +51,5 @@ Gem::Specification.new do |spec|
|
|
47
51
|
spec.add_development_dependency 'ruby-prof'
|
48
52
|
spec.add_development_dependency 'bcrypt'
|
49
53
|
spec.add_development_dependency 'thin'
|
54
|
+
spec.add_development_dependency 'memory_model'
|
50
55
|
end
|
@@ -3,10 +3,16 @@ module JSONAPIonify::Api
|
|
3
3
|
attr_reader :name, :request_block, :content_type, :responses, :prepend,
|
4
4
|
:path, :request_method, :only_associated
|
5
5
|
|
6
|
-
def self.
|
6
|
+
def self.dummy(&block)
|
7
7
|
new(nil, nil, &block)
|
8
8
|
end
|
9
9
|
|
10
|
+
def self.error(name, &block)
|
11
|
+
dummy do
|
12
|
+
error_now name, &block
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
10
16
|
def initialize(name, request_method, path = nil, require_body = nil, example_type = :resource, content_type: nil, prepend: nil, only_associated: false, &block)
|
11
17
|
@request_method = request_method
|
12
18
|
@require_body = require_body.nil? ? %w{POST PUT PATCH}.include?(@request_method) : require_body
|
@@ -54,17 +60,16 @@ module JSONAPIonify::Api
|
|
54
60
|
request.path_info.match(path_regex(base, name, include_path))
|
55
61
|
end
|
56
62
|
|
57
|
-
def documentation_object(
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
)
|
64
|
-
|
63
|
+
def documentation_object(base, resource, name, include_path, label)
|
64
|
+
url = build_path(base, name.to_s, include_path)
|
65
|
+
path = URI.parse(url).path
|
66
|
+
OpenStruct.new(
|
67
|
+
id: [request_method, path].join('-').parameterize,
|
68
|
+
label: label,
|
69
|
+
sample_requests: example_requests(resource, url)
|
70
|
+
)
|
65
71
|
end
|
66
72
|
|
67
|
-
|
68
73
|
def example_requests(resource, url)
|
69
74
|
responses.map do |response|
|
70
75
|
opts = {}
|
@@ -76,7 +81,7 @@ module JSONAPIonify::Api
|
|
76
81
|
{ 'data' => resource.build_resource(request, resource.example_instance, relationships: false, links: false).as_json }.to_json
|
77
82
|
when :resource_identifier
|
78
83
|
{ 'data' => resource.build_resource_identifier(resource.example_instance).as_json }.to_json
|
79
|
-
end if @content_type == 'application/vnd.api+json'
|
84
|
+
end if @content_type == 'application/vnd.api+json' && !%w{GET DELETE}.include?(request_method)
|
80
85
|
request = Server::Request.env_for(url, request_method, opts)
|
81
86
|
response = Server::MockResponse.new(*sample_request(resource, request))
|
82
87
|
|
@@ -88,8 +93,7 @@ module JSONAPIonify::Api
|
|
88
93
|
end
|
89
94
|
|
90
95
|
def supports_content_type?(request)
|
91
|
-
@content_type == request.content_type ||
|
92
|
-
(request.content_type.nil? && !request.has_body?)
|
96
|
+
@content_type == request.content_type || !request.has_body?
|
93
97
|
end
|
94
98
|
|
95
99
|
def supports_request_method?(request)
|
@@ -137,57 +141,63 @@ module JSONAPIonify::Api
|
|
137
141
|
end
|
138
142
|
|
139
143
|
def call(resource, request)
|
140
|
-
action
|
141
|
-
|
142
|
-
cache_options = {}
|
144
|
+
action = dup
|
145
|
+
cache_options = {}
|
143
146
|
resource.new.instance_eval do
|
144
147
|
# Bootstrap the Action
|
145
|
-
|
146
|
-
context = ContextDelegate.new(request, self, self.class.context_definitions)
|
147
|
-
|
148
|
-
define_singleton_method :action_name do
|
149
|
-
action.name
|
150
|
-
end
|
148
|
+
context = ContextDelegate.new(request, self, self.class.context_definitions)
|
151
149
|
|
150
|
+
# Define Singletons
|
152
151
|
define_singleton_method :cache do |key, **options|
|
152
|
+
raise Errors::DoubleCacheError, "Cache was already called for this action" if @called
|
153
|
+
@called = true
|
153
154
|
cache_options.merge! options
|
154
|
-
cache_options[:key] = [*{
|
155
|
-
api: [self.class.api.name, self.class.api.resource_signature].join('@'),
|
156
|
-
resource: self.class.type,
|
157
|
-
content_type: request.content_type || '*',
|
158
|
-
accept: request.accept.join(','),
|
159
|
-
params: context.params.to_param
|
160
|
-
}.map { |kv| kv.join(':') }, key].join('|')
|
161
|
-
raise cache_hit_exception, cache_options[:key] if self.class.cache_store.exist?(cache_options[:key])
|
162
|
-
end if request.get?
|
163
155
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
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]
|
168
172
|
end
|
173
|
+
end if request.get?
|
169
174
|
|
170
|
-
|
171
|
-
|
172
|
-
|
175
|
+
define_singleton_method :action_name do
|
176
|
+
action.name
|
177
|
+
end
|
173
178
|
|
174
|
-
|
175
|
-
|
176
|
-
|
179
|
+
define_singleton_method :errors do
|
180
|
+
context.errors
|
181
|
+
end
|
182
|
+
|
183
|
+
define_singleton_method :response_headers do
|
184
|
+
context.response_headers
|
177
185
|
end
|
178
186
|
|
179
187
|
begin
|
180
188
|
# Run Callbacks
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
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
|
186
196
|
end
|
187
197
|
|
188
198
|
# Start the request
|
189
199
|
instance_exec(context, &action.request_block)
|
190
|
-
fail
|
200
|
+
fail Errors::RequestError if errors.present?
|
191
201
|
response_definition =
|
192
202
|
action.responses.find { |response| response.accept? request } ||
|
193
203
|
error_now(:not_acceptable)
|
@@ -196,11 +206,11 @@ module JSONAPIonify::Api
|
|
196
206
|
cache_options[:key],
|
197
207
|
[status, headers, body.body],
|
198
208
|
**cache_options.except(:key)
|
199
|
-
) if request.get?
|
209
|
+
) if request.get? && cache_options.present?
|
200
210
|
end
|
201
|
-
rescue
|
211
|
+
rescue Errors::RequestError
|
202
212
|
error_response
|
203
|
-
rescue
|
213
|
+
rescue Errors::CacheHit
|
204
214
|
self.class.cache_store.read cache_options[:key]
|
205
215
|
rescue Exception => exception
|
206
216
|
rescued_response exception
|
@@ -1,8 +1,12 @@
|
|
1
|
+
require 'unstrict_proc'
|
2
|
+
|
1
3
|
module JSONAPIonify::Api
|
2
4
|
class Attribute
|
5
|
+
using UnstrictProc
|
3
6
|
attr_reader :name, :type, :description, :read, :write, :required
|
4
7
|
|
5
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
|
6
10
|
unless type.is_a? JSONAPIonify::Types::BaseType
|
7
11
|
raise TypeError, "#{type} is not a valid JSON type"
|
8
12
|
end
|
@@ -20,6 +24,13 @@ module JSONAPIonify::Api
|
|
20
24
|
self.name == other.name
|
21
25
|
end
|
22
26
|
|
27
|
+
def options_json
|
28
|
+
{
|
29
|
+
name: name,
|
30
|
+
required: required
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
23
34
|
def required?
|
24
35
|
!!@required
|
25
36
|
end
|
@@ -36,10 +47,10 @@ module JSONAPIonify::Api
|
|
36
47
|
!!@write
|
37
48
|
end
|
38
49
|
|
39
|
-
def example
|
50
|
+
def example(*args)
|
40
51
|
case @example
|
41
52
|
when Proc
|
42
|
-
type.dump @example.call
|
53
|
+
type.dump @example.unstrict.call(*args)
|
43
54
|
when nil
|
44
55
|
type.dump type.sample(name)
|
45
56
|
else
|
@@ -1,6 +1,17 @@
|
|
1
1
|
module JSONAPIonify::Api
|
2
2
|
module Base::AppBuilder
|
3
3
|
|
4
|
+
def self.extended(klass)
|
5
|
+
klass.class_eval do
|
6
|
+
extend JSONAPIonify::InheritedAttributes
|
7
|
+
inherited_array_attribute :middleware
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def use(*args, &block)
|
12
|
+
middleware << [args, block]
|
13
|
+
end
|
14
|
+
|
4
15
|
def call(env)
|
5
16
|
app.call(env)
|
6
17
|
end
|
@@ -15,16 +26,20 @@ module JSONAPIonify::Api
|
|
15
26
|
use Base::Reloader unless ENV['RACK_ENV'] == 'production'
|
16
27
|
map "/docs" do
|
17
28
|
run ->(env) {
|
18
|
-
request
|
29
|
+
request = JSONAPIonify::Api::Server::Request.new env
|
19
30
|
if request.path_info.present?
|
20
31
|
return [301, { 'location' => request.path.chomp(request.path_info) }, []]
|
21
32
|
end
|
22
|
-
response
|
33
|
+
response = Rack::Response.new
|
23
34
|
response.write api.documentation_output(request)
|
24
35
|
response.finish
|
25
36
|
}
|
26
37
|
end
|
27
38
|
map "/" do
|
39
|
+
use Rack::MethodOverride
|
40
|
+
api.middleware.each do |args, block|
|
41
|
+
use *args, &block
|
42
|
+
end
|
28
43
|
run JSONAPIonify::Api::Server.new(api)
|
29
44
|
end
|
30
45
|
end
|
@@ -4,45 +4,57 @@ module JSONAPIonify::Api
|
|
4
4
|
module Base::ClassMethods
|
5
5
|
|
6
6
|
def self.extended(klass)
|
7
|
-
klass.class_attribute :load_path
|
7
|
+
klass.class_attribute :load_path, :load_file
|
8
8
|
end
|
9
9
|
|
10
10
|
def resource_files
|
11
|
-
Dir.glob
|
11
|
+
files = Dir.glob(File.join(load_path, '**/*.rb'))
|
12
|
+
files.concat(superclass < JSONAPIonify::Api::Base ? superclass.resource_files : []).sort
|
12
13
|
end
|
13
14
|
|
14
15
|
def resource_signature
|
15
|
-
Digest::SHA2.hexdigest resource_files.map { |file| File.read file }.join
|
16
|
+
Digest::SHA2.hexdigest [*resource_files, load_file].map { |file| File.read file }.join
|
17
|
+
end
|
18
|
+
|
19
|
+
def signature
|
20
|
+
[name, resource_signature].join('@')
|
16
21
|
end
|
17
22
|
|
18
23
|
def load_resources
|
19
|
-
return unless load_path
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
require file
|
26
|
-
end
|
24
|
+
return unless load_path || resources_loaded?
|
25
|
+
@documentation_output = nil
|
26
|
+
@last_signature = resource_signature
|
27
|
+
$".delete_if { |s| s.start_with? load_path }
|
28
|
+
resource_files.each do |file|
|
29
|
+
require file
|
27
30
|
end
|
28
31
|
end
|
29
32
|
|
30
|
-
def
|
31
|
-
|
33
|
+
def resources_loaded?
|
34
|
+
@last_signature == resource_signature
|
35
|
+
end
|
36
|
+
|
37
|
+
def http_error(action, request)
|
38
|
+
Action.error(action).call(resource_class, request)
|
32
39
|
end
|
33
40
|
|
34
|
-
def
|
35
|
-
|
41
|
+
def root_url(request)
|
42
|
+
URI.parse(request.root_url).tap do |uri|
|
43
|
+
sticky_params = sticky_params(request.params)
|
44
|
+
uri.query = sticky_params.to_param if sticky_params.present?
|
45
|
+
end.to_s
|
36
46
|
end
|
37
47
|
|
38
48
|
def process_index(request)
|
39
49
|
headers = ContextDelegate.new(request, resource_class.new, resource_class.context_definitions).response_headers
|
40
50
|
obj = JSONAPIonify.new_object
|
41
51
|
obj[:meta] = { resources: {} }
|
42
|
-
obj[:links] = { self: request.
|
52
|
+
obj[:links] = { self: request.url }
|
43
53
|
obj[:meta][:documentation] = File.join(request.root_url, 'docs')
|
44
54
|
obj[:meta][:resources] = resources.each_with_object({}) do |resource, hash|
|
45
|
-
|
55
|
+
if resource.actions.any? { |action| action.name == :list }
|
56
|
+
hash[resource.type] = resource.get_url(root_url(request))
|
57
|
+
end
|
46
58
|
end
|
47
59
|
Rack::Response.new.tap do |response|
|
48
60
|
response.status = 200
|
@@ -66,6 +78,10 @@ module JSONAPIonify::Api
|
|
66
78
|
@cache_store = store
|
67
79
|
end
|
68
80
|
|
81
|
+
def eager_load
|
82
|
+
resources.each(&:eager_load)
|
83
|
+
end
|
84
|
+
|
69
85
|
def cache_store
|
70
86
|
@cache_store ||= JSONAPIonify.cache_store
|
71
87
|
end
|
@@ -5,7 +5,10 @@ module JSONAPIonify::Api
|
|
5
5
|
klass.class_eval do
|
6
6
|
class << self
|
7
7
|
delegate :context, :response_header, :helper, :rescue_from, :error,
|
8
|
-
:
|
8
|
+
:enable_pagination, :before, :param, :request_header,
|
9
|
+
:define_pagination_strategy, :define_sorting_strategy,
|
10
|
+
:sticky_params, :authentication, :on_exception,
|
11
|
+
:example_id_generator,
|
9
12
|
to: :resource_class
|
10
13
|
end
|
11
14
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
module JSONAPIonify::Api
|
2
2
|
module Base::DocHelper
|
3
|
+
Link = Struct.new(:title, :href)
|
3
4
|
|
4
5
|
def self.extended(klass)
|
5
6
|
klass.class_eval do
|
@@ -8,8 +9,12 @@ module JSONAPIonify::Api
|
|
8
9
|
end
|
9
10
|
end
|
10
11
|
|
11
|
-
def
|
12
|
-
|
12
|
+
def documentation_order(resources_in_order)
|
13
|
+
@documentation_order = resources_in_order
|
14
|
+
end
|
15
|
+
|
16
|
+
def link(title, href)
|
17
|
+
links << Link.new(title, href)
|
13
18
|
end
|
14
19
|
|
15
20
|
def title(title)
|
@@ -21,12 +26,16 @@ module JSONAPIonify::Api
|
|
21
26
|
end
|
22
27
|
|
23
28
|
def documentation_output(request)
|
24
|
-
|
29
|
+
#cache_store.fetch(resource_signature) do
|
30
|
+
JSONAPIonify::Documentation.new(documentation_object(request)).result
|
31
|
+
#end
|
25
32
|
end
|
26
33
|
|
27
34
|
def resources_in_order
|
28
35
|
indexes = @documentation_order || []
|
29
|
-
resources.sort_by
|
36
|
+
resources.sort_by(&:name).sort_by do |resource|
|
37
|
+
indexes.map(&:to_s).index(resource.type) || indexes.length
|
38
|
+
end
|
30
39
|
end
|
31
40
|
|
32
41
|
def documentation_object(request)
|
@@ -11,12 +11,16 @@ module JSONAPIonify::Api
|
|
11
11
|
def resource(type)
|
12
12
|
raise ArgumentError, 'type required' if type.nil?
|
13
13
|
type = type.to_sym
|
14
|
-
const_name = type.to_s.camelcase
|
14
|
+
const_name = type.to_s.camelcase + 'Resource'
|
15
15
|
return const_get(const_name, false) if const_defined?(const_name, false)
|
16
16
|
raise Errors::ResourceNotFound, "Resource not defined: #{type}" unless resource_defined?(type)
|
17
17
|
klass = Class.new(resource_class, &resource_definitions[type]).set_type(type)
|
18
18
|
param(:fields, type)
|
19
19
|
const_set const_name, klass
|
20
|
+
rescue Errors::ResourceNotFound => e
|
21
|
+
raise e if resources_loaded?
|
22
|
+
load_resources
|
23
|
+
retry
|
20
24
|
end
|
21
25
|
|
22
26
|
def resource_defined?(name)
|
@@ -30,10 +34,17 @@ module JSONAPIonify::Api
|
|
30
34
|
end
|
31
35
|
|
32
36
|
def define_resource(name, &block)
|
33
|
-
const_name = name.to_s.camelcase
|
37
|
+
const_name = name.to_s.camelcase + 'Resource'
|
34
38
|
remove_const(const_name) if const_defined? const_name
|
35
39
|
resource_definitions[name.to_sym] = block
|
36
40
|
end
|
37
41
|
|
42
|
+
def extend_resource(name, &block)
|
43
|
+
old = resource_definitions[name.to_sym]
|
44
|
+
resource_definitions[name.to_sym] = proc do
|
45
|
+
[old, block].each { |b| class_eval(&b) }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
38
49
|
end
|
39
50
|
end
|
@@ -13,12 +13,28 @@ module JSONAPIonify::Api
|
|
13
13
|
|
14
14
|
def self.inherited(subclass)
|
15
15
|
super(subclass)
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
16
|
+
subclass.instance_exec(self) do |superclass|
|
17
|
+
const_set(:ResourceBase, Class.new(superclass.resource_class))
|
18
|
+
resource_class.set_api(self)
|
19
|
+
|
20
|
+
file = caller.reject { |f| f.start_with? JSONAPIonify.path }[0].split(/\:\d/)[0]
|
21
|
+
dir = File.expand_path File.dirname(file)
|
22
|
+
basename = File.basename(file, File.extname(file))
|
23
|
+
self.load_path = File.join(dir, basename)
|
24
|
+
self.load_file = file
|
25
|
+
|
26
|
+
@title = superclass.instance_variable_get(:@title)
|
27
|
+
@description = superclass.instance_variable_get(:@description)
|
28
|
+
@documentation_order = superclass.instance_variable_get(:@documentation_order)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.resource_class
|
33
|
+
if const_defined?(:ResourceBase, false)
|
34
|
+
const_get(:ResourceBase, false)
|
35
|
+
else
|
36
|
+
const_set(:ResourceBase, Class.new(Resource))
|
37
|
+
end
|
22
38
|
end
|
23
39
|
|
24
40
|
end
|