jsonapionify 0.0.1.pre → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
[![Version](
|
2
|
+
[![Gem Version](https://badge.fury.io/rb/jsonapionify.svg)](https://badge.fury.io/rb/jsonapionify)
|
3
3
|
[![Build Status](https://travis-ci.org/brandfolder/jsonapionify.svg?branch=master)](https://travis-ci.org/brandfolder/jsonapionify)
|
4
4
|
[![Code Climate](https://codeclimate.com/repos/5672446f137f95309c0067c6/badges/a369f0a182ce111c8fcd/gpa.svg)](https://codeclimate.com/repos/5672446f137f95309c0067c6/feed)
|
5
5
|
[![Test Coverage](https://codeclimate.com/repos/5672446f137f95309c0067c6/badges/a369f0a182ce111c8fcd/coverage.svg)](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
|
+
[![Documentation Example](https://api.url2png.com/v6/P3CAE278FC306AA/50ef2ba09c77f6fb25dd7f179de2a704/png/?thumbnail_max_width=500&url=https%3A%2F%2Fapi.brandfolder.com%2Fv2%2Fdocs)](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
|