jsonapionify 0.0.1.pre

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