jsonapionify 0.0.1.pre
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 +7 -0
- data/.codeclimate.yml +29 -0
- data/.csslintrc +2 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +1171 -0
- data/.ruby-version +1 -0
- data/.travis.yml +10 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/Guardfile +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +34 -0
- data/TODO +13 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/config.ru +15 -0
- data/fixtures/documentation.json +364 -0
- data/jsonapionify.gemspec +50 -0
- data/lib/core_ext/boolean.rb +3 -0
- data/lib/jsonapionify/api/action.rb +211 -0
- data/lib/jsonapionify/api/attribute.rb +67 -0
- data/lib/jsonapionify/api/base/app_builder.rb +33 -0
- data/lib/jsonapionify/api/base/class_methods.rb +73 -0
- data/lib/jsonapionify/api/base/delegation.rb +15 -0
- data/lib/jsonapionify/api/base/doc_helper.rb +47 -0
- data/lib/jsonapionify/api/base/reloader.rb +10 -0
- data/lib/jsonapionify/api/base/resource_definitions.rb +39 -0
- data/lib/jsonapionify/api/base.rb +25 -0
- data/lib/jsonapionify/api/context.rb +14 -0
- data/lib/jsonapionify/api/context_delegate.rb +42 -0
- data/lib/jsonapionify/api/errors.rb +6 -0
- data/lib/jsonapionify/api/errors_object.rb +66 -0
- data/lib/jsonapionify/api/header_options.rb +13 -0
- data/lib/jsonapionify/api/param_options.rb +46 -0
- data/lib/jsonapionify/api/relationship/blocks.rb +41 -0
- data/lib/jsonapionify/api/relationship/many.rb +61 -0
- data/lib/jsonapionify/api/relationship/one.rb +36 -0
- data/lib/jsonapionify/api/relationship.rb +89 -0
- data/lib/jsonapionify/api/resource/builders.rb +81 -0
- data/lib/jsonapionify/api/resource/class_methods.rb +82 -0
- data/lib/jsonapionify/api/resource/defaults/actions.rb +11 -0
- data/lib/jsonapionify/api/resource/defaults/errors.rb +99 -0
- data/lib/jsonapionify/api/resource/defaults/request_contexts.rb +96 -0
- data/lib/jsonapionify/api/resource/defaults/response_contexts.rb +31 -0
- data/lib/jsonapionify/api/resource/defaults.rb +10 -0
- data/lib/jsonapionify/api/resource/definitions/actions.rb +196 -0
- data/lib/jsonapionify/api/resource/definitions/attributes.rb +51 -0
- data/lib/jsonapionify/api/resource/definitions/contexts.rb +16 -0
- data/lib/jsonapionify/api/resource/definitions/helpers.rb +9 -0
- data/lib/jsonapionify/api/resource/definitions/pagination.rb +79 -0
- data/lib/jsonapionify/api/resource/definitions/params.rb +49 -0
- data/lib/jsonapionify/api/resource/definitions/relationships.rb +42 -0
- data/lib/jsonapionify/api/resource/definitions/request_headers.rb +103 -0
- data/lib/jsonapionify/api/resource/definitions/response_headers.rb +22 -0
- data/lib/jsonapionify/api/resource/definitions/scopes.rb +50 -0
- data/lib/jsonapionify/api/resource/definitions/sorting.rb +85 -0
- data/lib/jsonapionify/api/resource/definitions.rb +14 -0
- data/lib/jsonapionify/api/resource/error_handling.rb +108 -0
- data/lib/jsonapionify/api/resource/http.rb +11 -0
- data/lib/jsonapionify/api/resource/includer.rb +4 -0
- data/lib/jsonapionify/api/resource.rb +35 -0
- data/lib/jsonapionify/api/response.rb +47 -0
- data/lib/jsonapionify/api/server/mock_response.rb +37 -0
- data/lib/jsonapionify/api/server/request.rb +78 -0
- data/lib/jsonapionify/api/server.rb +50 -0
- data/lib/jsonapionify/api/test_helper.rb +52 -0
- data/lib/jsonapionify/api.rb +9 -0
- data/lib/jsonapionify/autoload.rb +52 -0
- data/lib/jsonapionify/callbacks.rb +49 -0
- data/lib/jsonapionify/character_range.rb +41 -0
- data/lib/jsonapionify/continuation.rb +26 -0
- data/lib/jsonapionify/documentation/template.erb +487 -0
- data/lib/jsonapionify/documentation.rb +40 -0
- data/lib/jsonapionify/enumerable_observer.rb +91 -0
- data/lib/jsonapionify/indented_string.rb +27 -0
- data/lib/jsonapionify/inherited_attributes.rb +125 -0
- data/lib/jsonapionify/structure/collections/base.rb +104 -0
- data/lib/jsonapionify/structure/collections/errors.rb +7 -0
- data/lib/jsonapionify/structure/collections/included_resources.rb +39 -0
- data/lib/jsonapionify/structure/collections/resource_identifiers.rb +7 -0
- data/lib/jsonapionify/structure/collections/resources.rb +7 -0
- data/lib/jsonapionify/structure/helpers/errors.rb +71 -0
- data/lib/jsonapionify/structure/helpers/inherits_origin.rb +17 -0
- data/lib/jsonapionify/structure/helpers/member_names.rb +37 -0
- data/lib/jsonapionify/structure/helpers/meta_delegate.rb +16 -0
- data/lib/jsonapionify/structure/helpers/object_defaults.rb +123 -0
- data/lib/jsonapionify/structure/helpers/object_setters.rb +21 -0
- data/lib/jsonapionify/structure/helpers/pagination_links.rb +10 -0
- data/lib/jsonapionify/structure/helpers/validations.rb +296 -0
- data/lib/jsonapionify/structure/maps/base.rb +25 -0
- data/lib/jsonapionify/structure/maps/error_links.rb +7 -0
- data/lib/jsonapionify/structure/maps/links.rb +21 -0
- data/lib/jsonapionify/structure/maps/relationship_links.rb +11 -0
- data/lib/jsonapionify/structure/maps/relationships.rb +23 -0
- data/lib/jsonapionify/structure/maps/resource_links.rb +7 -0
- data/lib/jsonapionify/structure/maps/top_level_links.rb +10 -0
- data/lib/jsonapionify/structure/objects/attributes.rb +29 -0
- data/lib/jsonapionify/structure/objects/base.rb +166 -0
- data/lib/jsonapionify/structure/objects/error.rb +16 -0
- data/lib/jsonapionify/structure/objects/included_resource.rb +14 -0
- data/lib/jsonapionify/structure/objects/jsonapi.rb +7 -0
- data/lib/jsonapionify/structure/objects/link.rb +18 -0
- data/lib/jsonapionify/structure/objects/meta.rb +7 -0
- data/lib/jsonapionify/structure/objects/relationship.rb +20 -0
- data/lib/jsonapionify/structure/objects/resource.rb +45 -0
- data/lib/jsonapionify/structure/objects/resource_identifier.rb +40 -0
- data/lib/jsonapionify/structure/objects/source.rb +10 -0
- data/lib/jsonapionify/structure/objects/top_level.rb +105 -0
- data/lib/jsonapionify/structure.rb +27 -0
- data/lib/jsonapionify/types/array_type.rb +32 -0
- data/lib/jsonapionify/types/boolean_type.rb +22 -0
- data/lib/jsonapionify/types/date_string_type.rb +28 -0
- data/lib/jsonapionify/types/float_type.rb +8 -0
- data/lib/jsonapionify/types/integer_type.rb +9 -0
- data/lib/jsonapionify/types/object_type.rb +22 -0
- data/lib/jsonapionify/types/string_type.rb +66 -0
- data/lib/jsonapionify/types/time_string_type.rb +28 -0
- data/lib/jsonapionify/types.rb +49 -0
- data/lib/jsonapionify/unstrict_proc.rb +28 -0
- data/lib/jsonapionify/version.rb +3 -0
- data/lib/jsonapionify.rb +37 -0
- metadata +530 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
class Action
|
|
3
|
+
attr_reader :name, :request_block, :content_type, :responses, :prepend,
|
|
4
|
+
:path, :request_method, :only_associated
|
|
5
|
+
|
|
6
|
+
def self.stub(&block)
|
|
7
|
+
new(nil, nil, &block)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(name, request_method, path = nil, require_body = nil, example_type = :resource, content_type: nil, prepend: nil, only_associated: false, &block)
|
|
11
|
+
@request_method = request_method
|
|
12
|
+
@require_body = require_body.nil? ? %w{POST PUT PATCH}.include?(@request_method) : require_body
|
|
13
|
+
@path = path || ''
|
|
14
|
+
@prepend = prepend
|
|
15
|
+
@only_associated = only_associated
|
|
16
|
+
@name = name
|
|
17
|
+
@example_type = example_type
|
|
18
|
+
@content_type = content_type || 'application/vnd.api+json'
|
|
19
|
+
@request_block = block || proc {}
|
|
20
|
+
@responses = []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize_copy(new_instance)
|
|
24
|
+
super
|
|
25
|
+
%i{@responses}.each do |ivar|
|
|
26
|
+
value = instance_variable_get(ivar)
|
|
27
|
+
new_instance.instance_variable_set(
|
|
28
|
+
ivar, value.frozen? ? value : value.dup
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def build_path(base, name, include_path)
|
|
34
|
+
File.join(*[base].tap do |parts|
|
|
35
|
+
parts << prepend if prepend
|
|
36
|
+
parts << name
|
|
37
|
+
parts << path if path.present? && include_path
|
|
38
|
+
end)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def path_regex(base, name, include_path)
|
|
42
|
+
raw_reqexp = build_path(base, name, include_path).gsub(':id', '(?<id>[^\/]+)')
|
|
43
|
+
Regexp.new('^' + raw_reqexp + '$')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def ==(other)
|
|
47
|
+
self.class == other.class &&
|
|
48
|
+
%i{@request_method @path @content_type @prepend}.all? do |ivar|
|
|
49
|
+
instance_variable_get(ivar) == other.instance_variable_get(ivar)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def supports_path?(request, base, name, include_path)
|
|
54
|
+
request.path_info.match(path_regex(base, name, include_path))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def documentation_object(resource, base, name, include_path)
|
|
58
|
+
@documentation_object ||= begin
|
|
59
|
+
url = build_path(base, name, include_path)
|
|
60
|
+
OpenStruct.new(
|
|
61
|
+
name: self.name.to_s,
|
|
62
|
+
sample_requests: example_requests(resource, url)
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def example_requests(resource, url)
|
|
69
|
+
responses.map do |response|
|
|
70
|
+
opts = {}
|
|
71
|
+
opts['HTTP_CONTENT_TYPE'] = content_type if @require_body
|
|
72
|
+
opts['HTTP_ACCEPT'] = response.accept
|
|
73
|
+
request = Server::Request.env_for(url, request_method, opts)
|
|
74
|
+
opts[:input] = case @example_type
|
|
75
|
+
when :resource
|
|
76
|
+
{ 'data' => resource.build_resource(request, resource.example_instance, relationships: false, links: false).as_json }.to_json
|
|
77
|
+
when :resource_identifier
|
|
78
|
+
{ 'data' => resource.build_resource_identifier(resource.example_instance).as_json }.to_json
|
|
79
|
+
end if @content_type == 'application/vnd.api+json'
|
|
80
|
+
request = Server::Request.env_for(url, request_method, opts)
|
|
81
|
+
response = Server::MockResponse.new(*sample_request(resource, request))
|
|
82
|
+
|
|
83
|
+
OpenStruct.new(
|
|
84
|
+
request: request.http_string,
|
|
85
|
+
response: response.http_string
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def supports_content_type?(request)
|
|
91
|
+
@content_type == request.content_type ||
|
|
92
|
+
(request.content_type.nil? && !request.has_body?)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def supports_request_method?(request)
|
|
96
|
+
request.request_method == @request_method
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def supports?(request, base, name, include_path)
|
|
100
|
+
supports_path?(request, base, name, include_path) &&
|
|
101
|
+
supports_request_method?(request) &&
|
|
102
|
+
supports_content_type?(request)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def response(status: nil, accept: nil, &block)
|
|
106
|
+
new_response = Response.new(self, status: status, accept: accept, &block)
|
|
107
|
+
@responses.delete new_response
|
|
108
|
+
@responses << new_response
|
|
109
|
+
self
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def sample_request(resource, request)
|
|
113
|
+
action = dup
|
|
114
|
+
resource.new.instance_eval do
|
|
115
|
+
sample_context = self.class.context_definitions.dup
|
|
116
|
+
sample_context[:collection] =
|
|
117
|
+
Context.new proc { 3.times.map.each_with_index { |i| resource.example_instance(i + 1) } }, true
|
|
118
|
+
sample_context[:paginated_collection] = Context.new proc { |context| context.collection }
|
|
119
|
+
sample_context[:instance] = Context.new proc { |context| context.collection.first }
|
|
120
|
+
if sample_context.has_key? :owner_context
|
|
121
|
+
sample_context[:owner_context] = Context.new proc { ContextDelegate::Mock.new }, true
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Bootstrap the Action
|
|
125
|
+
context = ContextDelegate.new(request, self, sample_context)
|
|
126
|
+
|
|
127
|
+
define_singleton_method :response_headers do
|
|
128
|
+
context.response_headers
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Render the response
|
|
132
|
+
response_definition =
|
|
133
|
+
action.responses.find { |response| response.accept? request } ||
|
|
134
|
+
error_now(:not_acceptable)
|
|
135
|
+
response_definition.call(self, context)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def call(resource, request)
|
|
140
|
+
action = dup
|
|
141
|
+
cache_hit_exception = Class.new StandardError
|
|
142
|
+
cache_options = {}
|
|
143
|
+
resource.new.instance_eval do
|
|
144
|
+
# Bootstrap the Action
|
|
145
|
+
callbacks = resource.callbacks_for(action.name).new
|
|
146
|
+
context = ContextDelegate.new(request, self, self.class.context_definitions)
|
|
147
|
+
|
|
148
|
+
define_singleton_method :action_name do
|
|
149
|
+
action.name
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
define_singleton_method :cache do |key, **options|
|
|
153
|
+
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
|
+
|
|
164
|
+
# Define Shared Singletons
|
|
165
|
+
[self, callbacks].each do |target|
|
|
166
|
+
target.define_singleton_method :errors do
|
|
167
|
+
context.errors
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
define_singleton_method :response_headers do
|
|
171
|
+
context.response_headers
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
target.define_singleton_method :error_exception do
|
|
175
|
+
context.error_exception
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
begin
|
|
180
|
+
# Run Callbacks
|
|
181
|
+
case callbacks.run_callbacks(:request, context) { errors.present? }
|
|
182
|
+
when true # Boolean true means errors
|
|
183
|
+
raise error_exception
|
|
184
|
+
when nil # nil means no result, callback failed
|
|
185
|
+
error_now :internal_server_error
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Start the request
|
|
189
|
+
instance_exec(context, &action.request_block)
|
|
190
|
+
fail error_exception if errors.present?
|
|
191
|
+
response_definition =
|
|
192
|
+
action.responses.find { |response| response.accept? request } ||
|
|
193
|
+
error_now(:not_acceptable)
|
|
194
|
+
response_definition.call(self, context).tap do |status, headers, body|
|
|
195
|
+
self.class.cache_store.write(
|
|
196
|
+
cache_options[:key],
|
|
197
|
+
[status, headers, body.body],
|
|
198
|
+
**cache_options.except(:key)
|
|
199
|
+
) if request.get?
|
|
200
|
+
end
|
|
201
|
+
rescue error_exception
|
|
202
|
+
error_response
|
|
203
|
+
rescue cache_hit_exception
|
|
204
|
+
self.class.cache_store.read cache_options[:key]
|
|
205
|
+
rescue Exception => exception
|
|
206
|
+
rescued_response exception
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
class Attribute
|
|
3
|
+
attr_reader :name, :type, :description, :read, :write, :required
|
|
4
|
+
|
|
5
|
+
def initialize(name, type, description, read: true, write: true, required: false, example: nil)
|
|
6
|
+
unless type.is_a? JSONAPIonify::Types::BaseType
|
|
7
|
+
raise TypeError, "#{type} is not a valid JSON type"
|
|
8
|
+
end
|
|
9
|
+
@name = name
|
|
10
|
+
@type = type
|
|
11
|
+
@description = description
|
|
12
|
+
@example = example
|
|
13
|
+
@read = read
|
|
14
|
+
@write = write
|
|
15
|
+
@required = write ? required : false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def ==(other)
|
|
19
|
+
self.class == other.class &&
|
|
20
|
+
self.name == other.name
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def required?
|
|
24
|
+
!!@required
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def optional?
|
|
28
|
+
!required?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def read?
|
|
32
|
+
!!@read
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def write?
|
|
36
|
+
!!@write
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def example
|
|
40
|
+
case @example
|
|
41
|
+
when Proc
|
|
42
|
+
type.dump @example.call
|
|
43
|
+
when nil
|
|
44
|
+
type.dump type.sample(name)
|
|
45
|
+
else
|
|
46
|
+
type.dump @example
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def documentation_object
|
|
51
|
+
OpenStruct.new(
|
|
52
|
+
name: name,
|
|
53
|
+
type: type.name,
|
|
54
|
+
required: required?,
|
|
55
|
+
description: JSONAPIonify::Documentation.render_markdown(description),
|
|
56
|
+
allow: allow
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def allow
|
|
61
|
+
Array.new.tap do |ary|
|
|
62
|
+
ary << 'read' if read?
|
|
63
|
+
ary << 'write' if write?
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
module Base::AppBuilder
|
|
3
|
+
|
|
4
|
+
def call(env)
|
|
5
|
+
app.call(env)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def app
|
|
11
|
+
api = self
|
|
12
|
+
Rack::Builder.new do
|
|
13
|
+
use Rack::ShowExceptions
|
|
14
|
+
use Rack::CommonLogger
|
|
15
|
+
use Base::Reloader unless ENV['RACK_ENV'] == 'production'
|
|
16
|
+
map "/docs" do
|
|
17
|
+
run ->(env) {
|
|
18
|
+
request = JSONAPIonify::Api::Server::Request.new env
|
|
19
|
+
if request.path_info.present?
|
|
20
|
+
return [301, { 'location' => request.path.chomp(request.path_info) }, []]
|
|
21
|
+
end
|
|
22
|
+
response = Rack::Response.new
|
|
23
|
+
response.write api.documentation_output(request)
|
|
24
|
+
response.finish
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
map "/" do
|
|
28
|
+
run JSONAPIonify::Api::Server.new(api)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
require 'active_support/core_ext/class/attribute'
|
|
2
|
+
|
|
3
|
+
module JSONAPIonify::Api
|
|
4
|
+
module Base::ClassMethods
|
|
5
|
+
|
|
6
|
+
def self.extended(klass)
|
|
7
|
+
klass.class_attribute :load_path
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def resource_files
|
|
11
|
+
Dir.glob File.join(load_path, '**/*.rb')
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def resource_signature
|
|
15
|
+
Digest::SHA2.hexdigest resource_files.map { |file| File.read file }.join
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def load_resources
|
|
19
|
+
return unless load_path
|
|
20
|
+
if @last_signature != resource_signature
|
|
21
|
+
@documentation_output = nil
|
|
22
|
+
@last_signature = resource_signature
|
|
23
|
+
$".delete_if { |s| s.start_with? load_path }
|
|
24
|
+
resource_files.each do |file|
|
|
25
|
+
require file
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def resource_class
|
|
31
|
+
const_get(:ResourceBase, false)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def documentation_order(resources_in_order)
|
|
35
|
+
@documentation_order = resources_in_order
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def process_index(request)
|
|
39
|
+
headers = ContextDelegate.new(request, resource_class.new, resource_class.context_definitions).response_headers
|
|
40
|
+
obj = JSONAPIonify.new_object
|
|
41
|
+
obj[:meta] = { resources: {} }
|
|
42
|
+
obj[:links] = { self: request.root_url }
|
|
43
|
+
obj[:meta][:documentation] = File.join(request.root_url, 'docs')
|
|
44
|
+
obj[:meta][:resources] = resources.each_with_object({}) do |resource, hash|
|
|
45
|
+
hash[resource.type] = resource.get_url(request.root_url)
|
|
46
|
+
end
|
|
47
|
+
Rack::Response.new.tap do |response|
|
|
48
|
+
response.status = 200
|
|
49
|
+
headers.each { |k, v| response[k] = v }
|
|
50
|
+
response['content-type'] = 'application/vnd.api+json'
|
|
51
|
+
response.write obj.to_json
|
|
52
|
+
end.finish
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def fields
|
|
56
|
+
resources.each_with_object({}) do |resource, fields|
|
|
57
|
+
fields[resource.type.to_sym] = resource.fields
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def cache(store, *args)
|
|
62
|
+
self.cache_store = ActiveSupport::Cache.lookup_store(store, *args)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def cache_store=(store)
|
|
66
|
+
@cache_store = store
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def cache_store
|
|
70
|
+
@cache_store ||= JSONAPIonify.cache_store
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
module Base::Delegation
|
|
3
|
+
|
|
4
|
+
def self.extended(klass)
|
|
5
|
+
klass.class_eval do
|
|
6
|
+
class << self
|
|
7
|
+
delegate :context, :response_header, :helper, :rescue_from, :error,
|
|
8
|
+
:pagination, :before, :param, :request_header, :sorting,
|
|
9
|
+
to: :resource_class
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
module Base::DocHelper
|
|
3
|
+
|
|
4
|
+
def self.extended(klass)
|
|
5
|
+
klass.class_eval do
|
|
6
|
+
extend JSONAPIonify::InheritedAttributes
|
|
7
|
+
inherited_array_attribute :links
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def link(href)
|
|
12
|
+
links << href
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def title(title)
|
|
16
|
+
@title = title
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def description(description)
|
|
20
|
+
@description = description
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def documentation_output(request)
|
|
24
|
+
@documentation_output ||= JSONAPIonify::Documentation.new(documentation_object(request)).result
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def resources_in_order
|
|
28
|
+
indexes = @documentation_order || []
|
|
29
|
+
resources.sort_by { |resource| indexes.index(resource.name) || indexes.length }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def documentation_object(request)
|
|
33
|
+
base_url = URI.parse(request.url).tap do |uri|
|
|
34
|
+
uri.query = nil
|
|
35
|
+
uri.path.chomp! request.path_info
|
|
36
|
+
uri.path.chomp! '/docs'
|
|
37
|
+
end.to_s
|
|
38
|
+
OpenStruct.new(
|
|
39
|
+
links: links,
|
|
40
|
+
title: @title || self.name,
|
|
41
|
+
base_url: base_url,
|
|
42
|
+
description: JSONAPIonify::Documentation.render_markdown(@description || ''),
|
|
43
|
+
resources: resources_in_order.map { |r| r.documentation_object base_url }
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
module Base::ResourceDefinitions
|
|
3
|
+
|
|
4
|
+
def self.extended(klass)
|
|
5
|
+
klass.class_eval do
|
|
6
|
+
extend JSONAPIonify::InheritedAttributes
|
|
7
|
+
inherited_hash_attribute :resource_definitions
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def resource(type)
|
|
12
|
+
raise ArgumentError, 'type required' if type.nil?
|
|
13
|
+
type = type.to_sym
|
|
14
|
+
const_name = type.to_s.camelcase
|
|
15
|
+
return const_get(const_name, false) if const_defined?(const_name, false)
|
|
16
|
+
raise Errors::ResourceNotFound, "Resource not defined: #{type}" unless resource_defined?(type)
|
|
17
|
+
klass = Class.new(resource_class, &resource_definitions[type]).set_type(type)
|
|
18
|
+
param(:fields, type)
|
|
19
|
+
const_set const_name, klass
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def resource_defined?(name)
|
|
23
|
+
!!resource_definitions[name]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def resources
|
|
27
|
+
resource_definitions.map do |name, _|
|
|
28
|
+
resource(name)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def define_resource(name, &block)
|
|
33
|
+
const_name = name.to_s.camelcase
|
|
34
|
+
remove_const(const_name) if const_defined? const_name
|
|
35
|
+
resource_definitions[name.to_sym] = block
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require 'active_support/descendants_tracker'
|
|
2
|
+
require 'active_support/core_ext/module/delegation'
|
|
3
|
+
|
|
4
|
+
module JSONAPIonify::Api
|
|
5
|
+
class Base
|
|
6
|
+
extend JSONAPIonify::Autoload
|
|
7
|
+
autoload_all
|
|
8
|
+
extend AppBuilder
|
|
9
|
+
extend DocHelper
|
|
10
|
+
extend ClassMethods
|
|
11
|
+
extend Delegation
|
|
12
|
+
extend ResourceDefinitions
|
|
13
|
+
|
|
14
|
+
def self.inherited(subclass)
|
|
15
|
+
super(subclass)
|
|
16
|
+
file = caller.reject { |f| f.start_with? JSONAPIonify.path }[0].split(/\:\d/)[0]
|
|
17
|
+
dir = File.expand_path File.dirname(file)
|
|
18
|
+
basename = File.basename(file, File.extname(file))
|
|
19
|
+
self.load_path = File.join(dir, basename)
|
|
20
|
+
subclass.const_set(:ResourceBase, Class.new(Resource).set_api(subclass))
|
|
21
|
+
load_resources
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
class ContextDelegate
|
|
3
|
+
class Mock
|
|
4
|
+
def method_missing(*args, &block)
|
|
5
|
+
self
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def initialize(request, instance, definitions)
|
|
10
|
+
memo = {}
|
|
11
|
+
delegate = self
|
|
12
|
+
@definitions = definitions
|
|
13
|
+
|
|
14
|
+
define_singleton_method :request do
|
|
15
|
+
request
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
%i{initialize_dup initialize_clone}.each do |method|
|
|
19
|
+
define_singleton_method method do |copy|
|
|
20
|
+
memo.each do |k, v|
|
|
21
|
+
copy.public_send "#{k}=", v
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
define_singleton_method(:reset) do |key|
|
|
27
|
+
memo.delete(key)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
definitions.each do |name, context|
|
|
31
|
+
define_singleton_method name do
|
|
32
|
+
memo[name] ||= context.call(instance, delegate)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
define_singleton_method "#{name}=" do |value|
|
|
36
|
+
memo[name] = value
|
|
37
|
+
end unless context.readonly?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
|
2
|
+
|
|
3
|
+
module JSONAPIonify
|
|
4
|
+
module Api
|
|
5
|
+
class ErrorsObject
|
|
6
|
+
|
|
7
|
+
delegate :present?, to: :collection
|
|
8
|
+
|
|
9
|
+
class Evaluator
|
|
10
|
+
Structure::Objects::Error.permitted_keys.each do |key|
|
|
11
|
+
define_method(key) do |value|
|
|
12
|
+
@error[key] = value
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(error)
|
|
17
|
+
@error = error
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def meta
|
|
22
|
+
JSONAPIonify::Structure::Helpers::MetaDelegate.new @error
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def pointer(value)
|
|
26
|
+
@error[:source] ||= {}
|
|
27
|
+
@error[:source][:pointer] = value
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def parameter(value)
|
|
31
|
+
@error[:source] ||= {}
|
|
32
|
+
@error[:source][:parameter] = value
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def evaluate(*args, error_block:, runtime_block:, backtrace: nil)
|
|
37
|
+
backtrace ||= caller
|
|
38
|
+
error = Structure::Objects::Error.new
|
|
39
|
+
evaluator = Evaluator.new(error)
|
|
40
|
+
collection << error
|
|
41
|
+
[runtime_block, error_block].each do |block|
|
|
42
|
+
evaluator.instance_exec(*args, &block) if block
|
|
43
|
+
end
|
|
44
|
+
unless ENV['RACK_ENV'] == 'production'
|
|
45
|
+
error[:meta] ||= {}
|
|
46
|
+
error[:meta][:backtrace] = backtrace
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def top_level
|
|
51
|
+
JSONAPIonify.new_object.tap do |obj|
|
|
52
|
+
obj[:errors] = collection
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def collection
|
|
57
|
+
@collection ||= Structure::Collections::Errors.new
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def set(collection)
|
|
61
|
+
@collection = collection
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|