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.
Files changed (124) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +29 -0
  3. data/.csslintrc +2 -0
  4. data/.gitignore +11 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +1171 -0
  7. data/.ruby-version +1 -0
  8. data/.travis.yml +10 -0
  9. data/CODE_OF_CONDUCT.md +13 -0
  10. data/Gemfile +4 -0
  11. data/Guardfile +14 -0
  12. data/LICENSE.txt +21 -0
  13. data/README.md +43 -0
  14. data/Rakefile +34 -0
  15. data/TODO +13 -0
  16. data/bin/console +14 -0
  17. data/bin/setup +7 -0
  18. data/config.ru +15 -0
  19. data/fixtures/documentation.json +364 -0
  20. data/jsonapionify.gemspec +50 -0
  21. data/lib/core_ext/boolean.rb +3 -0
  22. data/lib/jsonapionify/api/action.rb +211 -0
  23. data/lib/jsonapionify/api/attribute.rb +67 -0
  24. data/lib/jsonapionify/api/base/app_builder.rb +33 -0
  25. data/lib/jsonapionify/api/base/class_methods.rb +73 -0
  26. data/lib/jsonapionify/api/base/delegation.rb +15 -0
  27. data/lib/jsonapionify/api/base/doc_helper.rb +47 -0
  28. data/lib/jsonapionify/api/base/reloader.rb +10 -0
  29. data/lib/jsonapionify/api/base/resource_definitions.rb +39 -0
  30. data/lib/jsonapionify/api/base.rb +25 -0
  31. data/lib/jsonapionify/api/context.rb +14 -0
  32. data/lib/jsonapionify/api/context_delegate.rb +42 -0
  33. data/lib/jsonapionify/api/errors.rb +6 -0
  34. data/lib/jsonapionify/api/errors_object.rb +66 -0
  35. data/lib/jsonapionify/api/header_options.rb +13 -0
  36. data/lib/jsonapionify/api/param_options.rb +46 -0
  37. data/lib/jsonapionify/api/relationship/blocks.rb +41 -0
  38. data/lib/jsonapionify/api/relationship/many.rb +61 -0
  39. data/lib/jsonapionify/api/relationship/one.rb +36 -0
  40. data/lib/jsonapionify/api/relationship.rb +89 -0
  41. data/lib/jsonapionify/api/resource/builders.rb +81 -0
  42. data/lib/jsonapionify/api/resource/class_methods.rb +82 -0
  43. data/lib/jsonapionify/api/resource/defaults/actions.rb +11 -0
  44. data/lib/jsonapionify/api/resource/defaults/errors.rb +99 -0
  45. data/lib/jsonapionify/api/resource/defaults/request_contexts.rb +96 -0
  46. data/lib/jsonapionify/api/resource/defaults/response_contexts.rb +31 -0
  47. data/lib/jsonapionify/api/resource/defaults.rb +10 -0
  48. data/lib/jsonapionify/api/resource/definitions/actions.rb +196 -0
  49. data/lib/jsonapionify/api/resource/definitions/attributes.rb +51 -0
  50. data/lib/jsonapionify/api/resource/definitions/contexts.rb +16 -0
  51. data/lib/jsonapionify/api/resource/definitions/helpers.rb +9 -0
  52. data/lib/jsonapionify/api/resource/definitions/pagination.rb +79 -0
  53. data/lib/jsonapionify/api/resource/definitions/params.rb +49 -0
  54. data/lib/jsonapionify/api/resource/definitions/relationships.rb +42 -0
  55. data/lib/jsonapionify/api/resource/definitions/request_headers.rb +103 -0
  56. data/lib/jsonapionify/api/resource/definitions/response_headers.rb +22 -0
  57. data/lib/jsonapionify/api/resource/definitions/scopes.rb +50 -0
  58. data/lib/jsonapionify/api/resource/definitions/sorting.rb +85 -0
  59. data/lib/jsonapionify/api/resource/definitions.rb +14 -0
  60. data/lib/jsonapionify/api/resource/error_handling.rb +108 -0
  61. data/lib/jsonapionify/api/resource/http.rb +11 -0
  62. data/lib/jsonapionify/api/resource/includer.rb +4 -0
  63. data/lib/jsonapionify/api/resource.rb +35 -0
  64. data/lib/jsonapionify/api/response.rb +47 -0
  65. data/lib/jsonapionify/api/server/mock_response.rb +37 -0
  66. data/lib/jsonapionify/api/server/request.rb +78 -0
  67. data/lib/jsonapionify/api/server.rb +50 -0
  68. data/lib/jsonapionify/api/test_helper.rb +52 -0
  69. data/lib/jsonapionify/api.rb +9 -0
  70. data/lib/jsonapionify/autoload.rb +52 -0
  71. data/lib/jsonapionify/callbacks.rb +49 -0
  72. data/lib/jsonapionify/character_range.rb +41 -0
  73. data/lib/jsonapionify/continuation.rb +26 -0
  74. data/lib/jsonapionify/documentation/template.erb +487 -0
  75. data/lib/jsonapionify/documentation.rb +40 -0
  76. data/lib/jsonapionify/enumerable_observer.rb +91 -0
  77. data/lib/jsonapionify/indented_string.rb +27 -0
  78. data/lib/jsonapionify/inherited_attributes.rb +125 -0
  79. data/lib/jsonapionify/structure/collections/base.rb +104 -0
  80. data/lib/jsonapionify/structure/collections/errors.rb +7 -0
  81. data/lib/jsonapionify/structure/collections/included_resources.rb +39 -0
  82. data/lib/jsonapionify/structure/collections/resource_identifiers.rb +7 -0
  83. data/lib/jsonapionify/structure/collections/resources.rb +7 -0
  84. data/lib/jsonapionify/structure/helpers/errors.rb +71 -0
  85. data/lib/jsonapionify/structure/helpers/inherits_origin.rb +17 -0
  86. data/lib/jsonapionify/structure/helpers/member_names.rb +37 -0
  87. data/lib/jsonapionify/structure/helpers/meta_delegate.rb +16 -0
  88. data/lib/jsonapionify/structure/helpers/object_defaults.rb +123 -0
  89. data/lib/jsonapionify/structure/helpers/object_setters.rb +21 -0
  90. data/lib/jsonapionify/structure/helpers/pagination_links.rb +10 -0
  91. data/lib/jsonapionify/structure/helpers/validations.rb +296 -0
  92. data/lib/jsonapionify/structure/maps/base.rb +25 -0
  93. data/lib/jsonapionify/structure/maps/error_links.rb +7 -0
  94. data/lib/jsonapionify/structure/maps/links.rb +21 -0
  95. data/lib/jsonapionify/structure/maps/relationship_links.rb +11 -0
  96. data/lib/jsonapionify/structure/maps/relationships.rb +23 -0
  97. data/lib/jsonapionify/structure/maps/resource_links.rb +7 -0
  98. data/lib/jsonapionify/structure/maps/top_level_links.rb +10 -0
  99. data/lib/jsonapionify/structure/objects/attributes.rb +29 -0
  100. data/lib/jsonapionify/structure/objects/base.rb +166 -0
  101. data/lib/jsonapionify/structure/objects/error.rb +16 -0
  102. data/lib/jsonapionify/structure/objects/included_resource.rb +14 -0
  103. data/lib/jsonapionify/structure/objects/jsonapi.rb +7 -0
  104. data/lib/jsonapionify/structure/objects/link.rb +18 -0
  105. data/lib/jsonapionify/structure/objects/meta.rb +7 -0
  106. data/lib/jsonapionify/structure/objects/relationship.rb +20 -0
  107. data/lib/jsonapionify/structure/objects/resource.rb +45 -0
  108. data/lib/jsonapionify/structure/objects/resource_identifier.rb +40 -0
  109. data/lib/jsonapionify/structure/objects/source.rb +10 -0
  110. data/lib/jsonapionify/structure/objects/top_level.rb +105 -0
  111. data/lib/jsonapionify/structure.rb +27 -0
  112. data/lib/jsonapionify/types/array_type.rb +32 -0
  113. data/lib/jsonapionify/types/boolean_type.rb +22 -0
  114. data/lib/jsonapionify/types/date_string_type.rb +28 -0
  115. data/lib/jsonapionify/types/float_type.rb +8 -0
  116. data/lib/jsonapionify/types/integer_type.rb +9 -0
  117. data/lib/jsonapionify/types/object_type.rb +22 -0
  118. data/lib/jsonapionify/types/string_type.rb +66 -0
  119. data/lib/jsonapionify/types/time_string_type.rb +28 -0
  120. data/lib/jsonapionify/types.rb +49 -0
  121. data/lib/jsonapionify/unstrict_proc.rb +28 -0
  122. data/lib/jsonapionify/version.rb +3 -0
  123. data/lib/jsonapionify.rb +37 -0
  124. 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,9 @@
1
+ module JSONAPIonify::Api
2
+ module Resource::Definitions::Helpers
3
+
4
+ def helper(name, &block)
5
+ define_method(name, &block)
6
+ end
7
+
8
+ end
9
+ 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