jsonapionify 0.0.1.pre
Sign up to get free protection for your applications and to get access to all the features.
- 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
|