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,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
|