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,196 @@
|
|
|
1
|
+
require 'active_support/core_ext/array/wrap'
|
|
2
|
+
|
|
3
|
+
module JSONAPIonify::Api
|
|
4
|
+
module Resource::Definitions::Actions
|
|
5
|
+
ActionNotFound = Class.new StandardError
|
|
6
|
+
|
|
7
|
+
def self.extended(klass)
|
|
8
|
+
klass.class_eval do
|
|
9
|
+
extend JSONAPIonify::InheritedAttributes
|
|
10
|
+
inherited_array_attribute :action_definitions
|
|
11
|
+
inherited_hash_attribute :callbacks
|
|
12
|
+
|
|
13
|
+
def self.inherited(subclass)
|
|
14
|
+
super
|
|
15
|
+
callbacks.each do |action_name, klass|
|
|
16
|
+
subclass.callbacks[action_name] = Class.new klass
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def list(**options, &block)
|
|
23
|
+
define_action(:list, 'GET', **options, &block).tap do |action|
|
|
24
|
+
action.response status: 200 do |context|
|
|
25
|
+
context.response_object[:data] = build_collection(
|
|
26
|
+
context.request,
|
|
27
|
+
context.response_collection,
|
|
28
|
+
fields: context.fields
|
|
29
|
+
)
|
|
30
|
+
context.meta[:total_count] = context.collection.count
|
|
31
|
+
context.response_object.to_json
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def index(**options, &block)
|
|
37
|
+
warn 'the `index` action will soon be deprecated, use `list` instead!'
|
|
38
|
+
list(**options, &block)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def create(**options, &block)
|
|
42
|
+
define_action(:create, 'POST', **options, &block).tap do |action|
|
|
43
|
+
action.response status: 201 do |context|
|
|
44
|
+
context.response_object[:data] = build_resource(context.request, context.instance, fields: context.fields)
|
|
45
|
+
context.response_object.to_json
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def read(**options, &block)
|
|
51
|
+
define_action(:read, 'GET', '/:id', **options, &block).tap do |action|
|
|
52
|
+
action.response status: 200 do |context|
|
|
53
|
+
context.response_object[:data] = build_resource(context.request, context.instance, fields: context.fields)
|
|
54
|
+
context.response_object.to_json
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def update(**options, &block)
|
|
60
|
+
define_action(:update, 'PATCH', '/:id', **options, &block).tap do |action|
|
|
61
|
+
action.response status: 200 do |context|
|
|
62
|
+
context.response_object[:data] = build_resource(context.request, context.instance, fields: context.fields)
|
|
63
|
+
context.response_object.to_json
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def delete(**options, &block)
|
|
69
|
+
define_action(:delete, 'DELETE', '/:id', **options, &block).tap do |action|
|
|
70
|
+
action.response status: 204
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def process(request)
|
|
75
|
+
path_actions = self.path_actions(request)
|
|
76
|
+
if request.options? && path_actions.present?
|
|
77
|
+
Action.stub do
|
|
78
|
+
headers['Allow'] = path_actions.map(&:request_method).join(', ')
|
|
79
|
+
response(status: 200, accept: '*/*')
|
|
80
|
+
end.call(self, request)
|
|
81
|
+
elsif (action = find_supported_action(request))
|
|
82
|
+
action.call(self, request)
|
|
83
|
+
elsif (rel = find_supported_relationship(request))
|
|
84
|
+
relationship(rel.name).process(request)
|
|
85
|
+
else
|
|
86
|
+
no_action_response(request).call(self, request)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def before(action_name = nil, &block)
|
|
91
|
+
if action_name == :index
|
|
92
|
+
warn 'the `index` action will soon be deprecated, use `list` instead!'
|
|
93
|
+
action_name = :list
|
|
94
|
+
end
|
|
95
|
+
return base_callbacks.before_request(&block) if action_name == nil
|
|
96
|
+
callbacks_for(action_name).before_request(&block)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def base_callbacks
|
|
100
|
+
resource = self
|
|
101
|
+
callbacks['*'] ||= Class.new do
|
|
102
|
+
def self.context(*)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
include Resource::ErrorHandling
|
|
106
|
+
|
|
107
|
+
define_singleton_method(:error_definitions) do
|
|
108
|
+
resource.error_definitions
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
include JSONAPIonify::Callbacks
|
|
112
|
+
define_callbacks :request
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def callbacks_for(action_name)
|
|
117
|
+
resource = self
|
|
118
|
+
callbacks[action_name] ||= Class.new(base_callbacks)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def define_action(*args, **options, &block)
|
|
122
|
+
Action.new(*args, **options, &block).tap do |new_action|
|
|
123
|
+
action_definitions.delete new_action
|
|
124
|
+
action_definitions << new_action
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def find_supported_action(request)
|
|
129
|
+
actions.find do |action|
|
|
130
|
+
action.supports?(request, base_path, path_name, supports_path?)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def no_action_response(request)
|
|
135
|
+
if request_method_actions(request).present?
|
|
136
|
+
Action.stub { error_now :unsupported_media_type }
|
|
137
|
+
elsif (path_actions = self.path_actions(request)).present?
|
|
138
|
+
Action.stub do
|
|
139
|
+
headers['Allow'] = path_actions.map(&:request_method).join(', ')
|
|
140
|
+
error_now :method_not_allowed
|
|
141
|
+
end
|
|
142
|
+
else
|
|
143
|
+
Action.stub { error_now :not_found }
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def path_actions(request)
|
|
148
|
+
actions.select do |action|
|
|
149
|
+
action.supports_path?(request, base_path, path_name, supports_path?)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def request_method_actions(request)
|
|
154
|
+
path_actions(request).select do |action|
|
|
155
|
+
action.supports_request_method?(request)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def find_supported_relationship(request)
|
|
160
|
+
relationship_definitions.find do |rel|
|
|
161
|
+
relationship(rel.name).path_actions(request).present?
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def remove_action(*names)
|
|
166
|
+
if names.include? :index
|
|
167
|
+
warn 'the `index` action will soon be deprecated, use `list` instead!'
|
|
168
|
+
names << :list
|
|
169
|
+
end
|
|
170
|
+
action_definitions.delete_if do |action_definition|
|
|
171
|
+
names.include? action_definition.name
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def actions
|
|
176
|
+
action_definitions.select do |action|
|
|
177
|
+
action.only_associated == false ||
|
|
178
|
+
(respond_to?(:rel) && action.only_associated == true)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private
|
|
183
|
+
|
|
184
|
+
def base_path
|
|
185
|
+
''
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def supports_path?
|
|
189
|
+
true
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def path_name
|
|
193
|
+
type.to_s
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
module Resource::Definitions::Attributes
|
|
3
|
+
|
|
4
|
+
def self.extended(klass)
|
|
5
|
+
klass.class_eval do
|
|
6
|
+
extend JSONAPIonify::InheritedAttributes
|
|
7
|
+
extend JSONAPIonify::Types
|
|
8
|
+
inherited_array_attribute :attributes
|
|
9
|
+
delegate :attributes, to: :class
|
|
10
|
+
|
|
11
|
+
context(:fields, readonly: true) do |context|
|
|
12
|
+
should_error = false
|
|
13
|
+
fields = (context.request.params['fields'] || {}).each_with_object(self.class.api.fields) do |(type, fields), field_map|
|
|
14
|
+
type_sym = type.to_sym
|
|
15
|
+
field_map[type_sym] =
|
|
16
|
+
fields.to_s.split(',').map(&:to_sym).each_with_object([]) do |field, field_list|
|
|
17
|
+
attribute = self.class.api.resource(type_sym).attributes.find do |attribute|
|
|
18
|
+
attribute.read? && attribute.name == field
|
|
19
|
+
end
|
|
20
|
+
attribute ? field_list << attribute.name : error(:field_not_permitted, type, field) && (should_error = true)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
raise error_exception if should_error
|
|
24
|
+
fields
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def id(sym)
|
|
30
|
+
define_singleton_method :id_attribute do
|
|
31
|
+
sym
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def attribute(name, type, description = '', **options)
|
|
36
|
+
Attribute.new(name, type, description, **options).tap do |new_attribute|
|
|
37
|
+
attributes.delete(new_attribute)
|
|
38
|
+
attributes << new_attribute
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def fields
|
|
43
|
+
attributes.select(&:read?).map(&:name)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def field_valid?(name)
|
|
47
|
+
fields.include? name.to_sym
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
module Resource::Definitions::Contexts
|
|
3
|
+
|
|
4
|
+
def self.extended(klass)
|
|
5
|
+
klass.class_eval do
|
|
6
|
+
extend JSONAPIonify::InheritedAttributes
|
|
7
|
+
inherited_hash_attribute :context_definitions
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def context(name, readonly: false, &block)
|
|
12
|
+
self.context_definitions[name.to_sym] = Context.new(block, readonly)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
module Resource::Definitions::Pagination
|
|
3
|
+
|
|
4
|
+
class PaginationLinksDelegate
|
|
5
|
+
|
|
6
|
+
def initialize(request, links)
|
|
7
|
+
@request = request
|
|
8
|
+
@links = links
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
%i{first last next prev}.each do |method|
|
|
12
|
+
define_method method do |**options|
|
|
13
|
+
@links[method] = URI.parse(@request.url).tap do |uri|
|
|
14
|
+
page_params = { page: options }.deep_stringify_keys
|
|
15
|
+
uri.query = @request.params.deep_merge(page_params).to_param
|
|
16
|
+
end.to_s
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
STRATEGIES = {
|
|
23
|
+
active_record: proc do |collection, params, links|
|
|
24
|
+
page_number = Integer(params['number'] || 1)
|
|
25
|
+
page_number = 1 if page_number < 1
|
|
26
|
+
page_size = Integer(params['size'] || 50)
|
|
27
|
+
raise PaginationError if page_size > 250
|
|
28
|
+
first_page = 1
|
|
29
|
+
last_page = (collection.count / page_size).ceil
|
|
30
|
+
last_page = 1 if last_page == 0
|
|
31
|
+
|
|
32
|
+
links.first number: 1 unless page_number == first_page
|
|
33
|
+
links.last number: last_page unless page_number == last_page
|
|
34
|
+
links.prev number: page_number - 1 unless page_number <= first_page
|
|
35
|
+
links.next number: page_number + 1 unless page_number >= last_page
|
|
36
|
+
|
|
37
|
+
slice_start = (page_number - 1) * page_size
|
|
38
|
+
collection.limit(page_size).offset(slice_start)
|
|
39
|
+
end,
|
|
40
|
+
enumerable: proc do |collection, params, links|
|
|
41
|
+
page_number = Integer(params['number'] || 1)
|
|
42
|
+
page_number = 1 if page_number < 1
|
|
43
|
+
page_size = Integer(params['size'] || 50)
|
|
44
|
+
first_page = 1
|
|
45
|
+
last_page = (collection.count / page_size).ceil
|
|
46
|
+
last_page = 1 if last_page == 0
|
|
47
|
+
|
|
48
|
+
links.first number: 1 unless page_number == first_page
|
|
49
|
+
links.last number: last_page unless page_number == last_page
|
|
50
|
+
links.prev number: page_number - 1 unless page_number <= first_page
|
|
51
|
+
links.next number: page_number + 1 unless page_number >= last_page
|
|
52
|
+
|
|
53
|
+
slice_start = (page_number - 1) * page_size
|
|
54
|
+
|
|
55
|
+
collection.slice(slice_start, page_size)
|
|
56
|
+
end
|
|
57
|
+
}
|
|
58
|
+
STRATEGIES[:array] = STRATEGIES[:enumerable]
|
|
59
|
+
DEFAULT = STRATEGIES[:enumerable]
|
|
60
|
+
|
|
61
|
+
def pagination(*params, strategy: nil, &block)
|
|
62
|
+
params = %i{number size} unless block
|
|
63
|
+
params.each { |p| param :page, p, actions: %i{list} }
|
|
64
|
+
context :paginated_collection do |context|
|
|
65
|
+
unless (actual_block = block)
|
|
66
|
+
actual_strategy = strategy || self.class.default_strategy
|
|
67
|
+
actual_block = actual_strategy ? STRATEGIES[actual_strategy] : DEFAULT
|
|
68
|
+
end
|
|
69
|
+
Object.new.instance_exec(
|
|
70
|
+
context.respond_to?(:sorted_collection) ? context.sorted_collection : context.collection,
|
|
71
|
+
context.request.params['page'] || {},
|
|
72
|
+
PaginationLinksDelegate.new(context.request, context.links),
|
|
73
|
+
&actual_block
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
module Resource::Definitions::Params
|
|
3
|
+
|
|
4
|
+
def self.extended(klass)
|
|
5
|
+
klass.class_eval do
|
|
6
|
+
extend JSONAPIonify::InheritedAttributes
|
|
7
|
+
inherited_hash_attribute :param_definitions
|
|
8
|
+
|
|
9
|
+
before do |context|
|
|
10
|
+
context.params # pull params so they verify
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
context(:params, readonly: true) do |context|
|
|
14
|
+
should_error = false
|
|
15
|
+
|
|
16
|
+
# Check for validity
|
|
17
|
+
params = self.class.param_definitions.select do |_, v|
|
|
18
|
+
v.actions.blank? || v.actions.include?(action_name)
|
|
19
|
+
end
|
|
20
|
+
required_params = params.select do |_, v|
|
|
21
|
+
v.required
|
|
22
|
+
end
|
|
23
|
+
if (invalid_params = ParamOptions.invalid_parameters(context.request.params, params.values.map(&:keypath))).present?
|
|
24
|
+
should_error = true
|
|
25
|
+
invalid_params.each do |string|
|
|
26
|
+
error :parameter_not_permitted, string
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Check for requirement
|
|
31
|
+
if (missing_params = ParamOptions.missing_parameters(context.request.params, required_params.values.map(&:keypath))).present?
|
|
32
|
+
error :parameters_missing, missing_params
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
raise error_exception if should_error
|
|
36
|
+
|
|
37
|
+
# Return the params
|
|
38
|
+
context.request.params
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def param(*keypath, **options)
|
|
45
|
+
param_definitions[keypath] = ParamOptions.new(*keypath, **options)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
module Resource::Definitions::Relationships
|
|
3
|
+
|
|
4
|
+
def self.extended(klass)
|
|
5
|
+
klass.class_eval do
|
|
6
|
+
extend JSONAPIonify::InheritedAttributes
|
|
7
|
+
inherited_array_attribute :relationship_definitions
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def relates_to_many(name, resource: nil, &block)
|
|
12
|
+
define_relationship(name, Relationship::Many, resource: resource, &block)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def relates_to_one(name, resource: nil, &block)
|
|
16
|
+
define_relationship(name, Relationship::One, resource: resource, &block)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def define_relationship(name, klass, resource: nil, &block)
|
|
20
|
+
const_name = name.to_s.camelcase + 'Relationship'
|
|
21
|
+
remove_const(const_name) if const_defined? const_name
|
|
22
|
+
klass.new(self, name, resource: resource, &block).tap do |new_relationship|
|
|
23
|
+
relationship_definitions.delete new_relationship
|
|
24
|
+
relationship_definitions << new_relationship
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def relationships
|
|
29
|
+
relationship_definitions
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def relationship(name)
|
|
33
|
+
name = name.to_sym
|
|
34
|
+
const_name = name.to_s.camelcase + 'Relationship'
|
|
35
|
+
return const_get(const_name, false) if const_defined? const_name
|
|
36
|
+
relationship_definition = relationship_definitions.find { |rel| rel.name == name }
|
|
37
|
+
raise Errors::RelationshipNotDefined, "Relationship not defined: #{name}" unless relationship_definition
|
|
38
|
+
const_set const_name, relationship_definition.resource_class
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
module Resource::Definitions::RequestHeaders
|
|
3
|
+
|
|
4
|
+
def self.extended(klass)
|
|
5
|
+
klass.class_eval do
|
|
6
|
+
extend JSONAPIonify::InheritedAttributes
|
|
7
|
+
inherited_hash_attribute :request_header_definitions
|
|
8
|
+
|
|
9
|
+
# Standard HTTP Headers
|
|
10
|
+
# https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields
|
|
11
|
+
request_header 'accept'
|
|
12
|
+
request_header 'accept-charset'
|
|
13
|
+
request_header 'accept-encoding'
|
|
14
|
+
request_header 'accept-language'
|
|
15
|
+
request_header 'accept-datetime'
|
|
16
|
+
request_header 'authorization'
|
|
17
|
+
request_header 'cache-control'
|
|
18
|
+
request_header 'connection'
|
|
19
|
+
request_header 'cookie'
|
|
20
|
+
request_header 'content-length'
|
|
21
|
+
request_header 'content-md5'
|
|
22
|
+
request_header 'content-type'
|
|
23
|
+
request_header 'date'
|
|
24
|
+
request_header 'expect'
|
|
25
|
+
request_header 'from'
|
|
26
|
+
request_header 'host'
|
|
27
|
+
request_header 'if-match'
|
|
28
|
+
request_header 'if-modified-since'
|
|
29
|
+
request_header 'if-none-match'
|
|
30
|
+
request_header 'if-range'
|
|
31
|
+
request_header 'if-unmodified-since'
|
|
32
|
+
request_header 'max-forwards'
|
|
33
|
+
request_header 'origin'
|
|
34
|
+
request_header 'pragma'
|
|
35
|
+
request_header 'proxy-authorization'
|
|
36
|
+
request_header 'range'
|
|
37
|
+
request_header 'referer'
|
|
38
|
+
request_header 'te'
|
|
39
|
+
request_header 'user-agent'
|
|
40
|
+
request_header 'upgrade'
|
|
41
|
+
request_header 'via'
|
|
42
|
+
request_header 'warning'
|
|
43
|
+
|
|
44
|
+
# Non-Standard, but widely used HTTP headers
|
|
45
|
+
request_header 'x-requested-with'
|
|
46
|
+
request_header 'dnt'
|
|
47
|
+
request_header 'x-forwarded-for'
|
|
48
|
+
request_header 'x-forwarded-host'
|
|
49
|
+
request_header 'x-forwarded-proto'
|
|
50
|
+
request_header 'front-end-https'
|
|
51
|
+
request_header 'x-att-device-id'
|
|
52
|
+
request_header 'x-wap-profile'
|
|
53
|
+
request_header 'proxy-connection'
|
|
54
|
+
request_header 'x-uidh'
|
|
55
|
+
request_header 'upgrade-insecure-requests'
|
|
56
|
+
|
|
57
|
+
# Don't allow method overrides
|
|
58
|
+
# request_header 'x-http-method-override'
|
|
59
|
+
|
|
60
|
+
# Don't allow CSRF tokens, as they should not be used
|
|
61
|
+
# in the api by default
|
|
62
|
+
# request_header 'x-csrf-token'
|
|
63
|
+
|
|
64
|
+
before do |context|
|
|
65
|
+
context.request_headers # pull request_headers so they verify
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
context(:request_headers) do |context|
|
|
69
|
+
should_error = false
|
|
70
|
+
|
|
71
|
+
# Check for validity
|
|
72
|
+
headers = self.class.request_header_definitions.select do |_, v|
|
|
73
|
+
v.actions.blank? || v.actions.include?(action_name)
|
|
74
|
+
end
|
|
75
|
+
required_headers = headers.select do |_, v|
|
|
76
|
+
v.required
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
if (invalid_keys = context.request.headers.keys.map(&:downcase) - headers.keys.map(&:downcase)).present?
|
|
80
|
+
should_error = true
|
|
81
|
+
invalid_keys.each do |key|
|
|
82
|
+
error :header_not_permitted, key
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
if (missing_keys = required_headers.keys.map(&:downcase) - context.request.headers.keys.map(&:downcase)).present?
|
|
87
|
+
should_error = true
|
|
88
|
+
error :headers_missing, missing_keys
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
raise error_exception if should_error
|
|
92
|
+
|
|
93
|
+
context.request.headers
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def request_header(name, **options)
|
|
99
|
+
request_header_definitions[name] = HeaderOptions.new(name, **options)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
module Resource::Definitions::ResponseHeaders
|
|
3
|
+
|
|
4
|
+
def self.extended(klass)
|
|
5
|
+
klass.class_eval do
|
|
6
|
+
extend JSONAPIonify::InheritedAttributes
|
|
7
|
+
inherited_hash_attribute :response_header_definitions
|
|
8
|
+
|
|
9
|
+
context(:response_headers) do |context|
|
|
10
|
+
self.class.response_header_definitions.each_with_object({}) do |(name, block), headers|
|
|
11
|
+
headers[name.to_s] = instance_exec(context, &block)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def response_header(name, &block)
|
|
18
|
+
self.response_header_definitions[name] = block
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
module Resource::Definitions::Scopes
|
|
3
|
+
|
|
4
|
+
def self.extended(klass)
|
|
5
|
+
klass.class_eval do
|
|
6
|
+
id :id
|
|
7
|
+
scope { raise NotImplementedError, 'scope not implemented' }
|
|
8
|
+
collection { raise NotImplementedError, 'collection not implemented' }
|
|
9
|
+
instance { raise NotImplementedError, 'instance not implemented' }
|
|
10
|
+
new_instance { raise NotImplementedError, 'new instance not implemented' }
|
|
11
|
+
param :include
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def scope(&block)
|
|
16
|
+
define_singleton_method(:current_scope) do
|
|
17
|
+
Object.new.instance_eval(&block)
|
|
18
|
+
end
|
|
19
|
+
context :scope do
|
|
20
|
+
self.class.current_scope
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
alias_method :resource_class, :scope
|
|
25
|
+
|
|
26
|
+
def instance(&block)
|
|
27
|
+
define_singleton_method(:find_instance) do |id|
|
|
28
|
+
Object.new.instance_exec(current_scope, id, &block)
|
|
29
|
+
end
|
|
30
|
+
context :instance do |context|
|
|
31
|
+
self.class.find_instance(context.id)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def collection(&block)
|
|
36
|
+
context :collection do |context|
|
|
37
|
+
Object.new.instance_exec(context.scope, context, &block)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def new_instance(&block)
|
|
42
|
+
define_singleton_method(:build_instance) do
|
|
43
|
+
Object.new.instance_exec(current_scope, &block)
|
|
44
|
+
end
|
|
45
|
+
context :new_instance do |context|
|
|
46
|
+
Object.new.instance_exec(context.scope, context, &block)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|