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,211 @@
1
+ module JSONAPIonify::Api
2
+ class Action
3
+ attr_reader :name, :request_block, :content_type, :responses, :prepend,
4
+ :path, :request_method, :only_associated
5
+
6
+ def self.stub(&block)
7
+ new(nil, nil, &block)
8
+ end
9
+
10
+ def initialize(name, request_method, path = nil, require_body = nil, example_type = :resource, content_type: nil, prepend: nil, only_associated: false, &block)
11
+ @request_method = request_method
12
+ @require_body = require_body.nil? ? %w{POST PUT PATCH}.include?(@request_method) : require_body
13
+ @path = path || ''
14
+ @prepend = prepend
15
+ @only_associated = only_associated
16
+ @name = name
17
+ @example_type = example_type
18
+ @content_type = content_type || 'application/vnd.api+json'
19
+ @request_block = block || proc {}
20
+ @responses = []
21
+ end
22
+
23
+ def initialize_copy(new_instance)
24
+ super
25
+ %i{@responses}.each do |ivar|
26
+ value = instance_variable_get(ivar)
27
+ new_instance.instance_variable_set(
28
+ ivar, value.frozen? ? value : value.dup
29
+ )
30
+ end
31
+ end
32
+
33
+ def build_path(base, name, include_path)
34
+ File.join(*[base].tap do |parts|
35
+ parts << prepend if prepend
36
+ parts << name
37
+ parts << path if path.present? && include_path
38
+ end)
39
+ end
40
+
41
+ def path_regex(base, name, include_path)
42
+ raw_reqexp = build_path(base, name, include_path).gsub(':id', '(?<id>[^\/]+)')
43
+ Regexp.new('^' + raw_reqexp + '$')
44
+ end
45
+
46
+ def ==(other)
47
+ self.class == other.class &&
48
+ %i{@request_method @path @content_type @prepend}.all? do |ivar|
49
+ instance_variable_get(ivar) == other.instance_variable_get(ivar)
50
+ end
51
+ end
52
+
53
+ def supports_path?(request, base, name, include_path)
54
+ request.path_info.match(path_regex(base, name, include_path))
55
+ end
56
+
57
+ def documentation_object(resource, base, name, include_path)
58
+ @documentation_object ||= begin
59
+ url = build_path(base, name, include_path)
60
+ OpenStruct.new(
61
+ name: self.name.to_s,
62
+ sample_requests: example_requests(resource, url)
63
+ )
64
+ end
65
+ end
66
+
67
+
68
+ def example_requests(resource, url)
69
+ responses.map do |response|
70
+ opts = {}
71
+ opts['HTTP_CONTENT_TYPE'] = content_type if @require_body
72
+ opts['HTTP_ACCEPT'] = response.accept
73
+ request = Server::Request.env_for(url, request_method, opts)
74
+ opts[:input] = case @example_type
75
+ when :resource
76
+ { 'data' => resource.build_resource(request, resource.example_instance, relationships: false, links: false).as_json }.to_json
77
+ when :resource_identifier
78
+ { 'data' => resource.build_resource_identifier(resource.example_instance).as_json }.to_json
79
+ end if @content_type == 'application/vnd.api+json'
80
+ request = Server::Request.env_for(url, request_method, opts)
81
+ response = Server::MockResponse.new(*sample_request(resource, request))
82
+
83
+ OpenStruct.new(
84
+ request: request.http_string,
85
+ response: response.http_string
86
+ )
87
+ end
88
+ end
89
+
90
+ def supports_content_type?(request)
91
+ @content_type == request.content_type ||
92
+ (request.content_type.nil? && !request.has_body?)
93
+ end
94
+
95
+ def supports_request_method?(request)
96
+ request.request_method == @request_method
97
+ end
98
+
99
+ def supports?(request, base, name, include_path)
100
+ supports_path?(request, base, name, include_path) &&
101
+ supports_request_method?(request) &&
102
+ supports_content_type?(request)
103
+ end
104
+
105
+ def response(status: nil, accept: nil, &block)
106
+ new_response = Response.new(self, status: status, accept: accept, &block)
107
+ @responses.delete new_response
108
+ @responses << new_response
109
+ self
110
+ end
111
+
112
+ def sample_request(resource, request)
113
+ action = dup
114
+ resource.new.instance_eval do
115
+ sample_context = self.class.context_definitions.dup
116
+ sample_context[:collection] =
117
+ Context.new proc { 3.times.map.each_with_index { |i| resource.example_instance(i + 1) } }, true
118
+ sample_context[:paginated_collection] = Context.new proc { |context| context.collection }
119
+ sample_context[:instance] = Context.new proc { |context| context.collection.first }
120
+ if sample_context.has_key? :owner_context
121
+ sample_context[:owner_context] = Context.new proc { ContextDelegate::Mock.new }, true
122
+ end
123
+
124
+ # Bootstrap the Action
125
+ context = ContextDelegate.new(request, self, sample_context)
126
+
127
+ define_singleton_method :response_headers do
128
+ context.response_headers
129
+ end
130
+
131
+ # Render the response
132
+ response_definition =
133
+ action.responses.find { |response| response.accept? request } ||
134
+ error_now(:not_acceptable)
135
+ response_definition.call(self, context)
136
+ end
137
+ end
138
+
139
+ def call(resource, request)
140
+ action = dup
141
+ cache_hit_exception = Class.new StandardError
142
+ cache_options = {}
143
+ resource.new.instance_eval do
144
+ # Bootstrap the Action
145
+ callbacks = resource.callbacks_for(action.name).new
146
+ context = ContextDelegate.new(request, self, self.class.context_definitions)
147
+
148
+ define_singleton_method :action_name do
149
+ action.name
150
+ end
151
+
152
+ define_singleton_method :cache do |key, **options|
153
+ cache_options.merge! options
154
+ cache_options[:key] = [*{
155
+ api: [self.class.api.name, self.class.api.resource_signature].join('@'),
156
+ resource: self.class.type,
157
+ content_type: request.content_type || '*',
158
+ accept: request.accept.join(','),
159
+ params: context.params.to_param
160
+ }.map { |kv| kv.join(':') }, key].join('|')
161
+ raise cache_hit_exception, cache_options[:key] if self.class.cache_store.exist?(cache_options[:key])
162
+ end if request.get?
163
+
164
+ # Define Shared Singletons
165
+ [self, callbacks].each do |target|
166
+ target.define_singleton_method :errors do
167
+ context.errors
168
+ end
169
+
170
+ define_singleton_method :response_headers do
171
+ context.response_headers
172
+ end
173
+
174
+ target.define_singleton_method :error_exception do
175
+ context.error_exception
176
+ end
177
+ end
178
+
179
+ begin
180
+ # Run Callbacks
181
+ case callbacks.run_callbacks(:request, context) { errors.present? }
182
+ when true # Boolean true means errors
183
+ raise error_exception
184
+ when nil # nil means no result, callback failed
185
+ error_now :internal_server_error
186
+ end
187
+
188
+ # Start the request
189
+ instance_exec(context, &action.request_block)
190
+ fail error_exception if errors.present?
191
+ response_definition =
192
+ action.responses.find { |response| response.accept? request } ||
193
+ error_now(:not_acceptable)
194
+ response_definition.call(self, context).tap do |status, headers, body|
195
+ self.class.cache_store.write(
196
+ cache_options[:key],
197
+ [status, headers, body.body],
198
+ **cache_options.except(:key)
199
+ ) if request.get?
200
+ end
201
+ rescue error_exception
202
+ error_response
203
+ rescue cache_hit_exception
204
+ self.class.cache_store.read cache_options[:key]
205
+ rescue Exception => exception
206
+ rescued_response exception
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,67 @@
1
+ module JSONAPIonify::Api
2
+ class Attribute
3
+ attr_reader :name, :type, :description, :read, :write, :required
4
+
5
+ def initialize(name, type, description, read: true, write: true, required: false, example: nil)
6
+ unless type.is_a? JSONAPIonify::Types::BaseType
7
+ raise TypeError, "#{type} is not a valid JSON type"
8
+ end
9
+ @name = name
10
+ @type = type
11
+ @description = description
12
+ @example = example
13
+ @read = read
14
+ @write = write
15
+ @required = write ? required : false
16
+ end
17
+
18
+ def ==(other)
19
+ self.class == other.class &&
20
+ self.name == other.name
21
+ end
22
+
23
+ def required?
24
+ !!@required
25
+ end
26
+
27
+ def optional?
28
+ !required?
29
+ end
30
+
31
+ def read?
32
+ !!@read
33
+ end
34
+
35
+ def write?
36
+ !!@write
37
+ end
38
+
39
+ def example
40
+ case @example
41
+ when Proc
42
+ type.dump @example.call
43
+ when nil
44
+ type.dump type.sample(name)
45
+ else
46
+ type.dump @example
47
+ end
48
+ end
49
+
50
+ def documentation_object
51
+ OpenStruct.new(
52
+ name: name,
53
+ type: type.name,
54
+ required: required?,
55
+ description: JSONAPIonify::Documentation.render_markdown(description),
56
+ allow: allow
57
+ )
58
+ end
59
+
60
+ def allow
61
+ Array.new.tap do |ary|
62
+ ary << 'read' if read?
63
+ ary << 'write' if write?
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,33 @@
1
+ module JSONAPIonify::Api
2
+ module Base::AppBuilder
3
+
4
+ def call(env)
5
+ app.call(env)
6
+ end
7
+
8
+ private
9
+
10
+ def app
11
+ api = self
12
+ Rack::Builder.new do
13
+ use Rack::ShowExceptions
14
+ use Rack::CommonLogger
15
+ use Base::Reloader unless ENV['RACK_ENV'] == 'production'
16
+ map "/docs" do
17
+ run ->(env) {
18
+ request = JSONAPIonify::Api::Server::Request.new env
19
+ if request.path_info.present?
20
+ return [301, { 'location' => request.path.chomp(request.path_info) }, []]
21
+ end
22
+ response = Rack::Response.new
23
+ response.write api.documentation_output(request)
24
+ response.finish
25
+ }
26
+ end
27
+ map "/" do
28
+ run JSONAPIonify::Api::Server.new(api)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,73 @@
1
+ require 'active_support/core_ext/class/attribute'
2
+
3
+ module JSONAPIonify::Api
4
+ module Base::ClassMethods
5
+
6
+ def self.extended(klass)
7
+ klass.class_attribute :load_path
8
+ end
9
+
10
+ def resource_files
11
+ Dir.glob File.join(load_path, '**/*.rb')
12
+ end
13
+
14
+ def resource_signature
15
+ Digest::SHA2.hexdigest resource_files.map { |file| File.read file }.join
16
+ end
17
+
18
+ def load_resources
19
+ return unless load_path
20
+ if @last_signature != resource_signature
21
+ @documentation_output = nil
22
+ @last_signature = resource_signature
23
+ $".delete_if { |s| s.start_with? load_path }
24
+ resource_files.each do |file|
25
+ require file
26
+ end
27
+ end
28
+ end
29
+
30
+ def resource_class
31
+ const_get(:ResourceBase, false)
32
+ end
33
+
34
+ def documentation_order(resources_in_order)
35
+ @documentation_order = resources_in_order
36
+ end
37
+
38
+ def process_index(request)
39
+ headers = ContextDelegate.new(request, resource_class.new, resource_class.context_definitions).response_headers
40
+ obj = JSONAPIonify.new_object
41
+ obj[:meta] = { resources: {} }
42
+ obj[:links] = { self: request.root_url }
43
+ obj[:meta][:documentation] = File.join(request.root_url, 'docs')
44
+ obj[:meta][:resources] = resources.each_with_object({}) do |resource, hash|
45
+ hash[resource.type] = resource.get_url(request.root_url)
46
+ end
47
+ Rack::Response.new.tap do |response|
48
+ response.status = 200
49
+ headers.each { |k, v| response[k] = v }
50
+ response['content-type'] = 'application/vnd.api+json'
51
+ response.write obj.to_json
52
+ end.finish
53
+ end
54
+
55
+ def fields
56
+ resources.each_with_object({}) do |resource, fields|
57
+ fields[resource.type.to_sym] = resource.fields
58
+ end
59
+ end
60
+
61
+ def cache(store, *args)
62
+ self.cache_store = ActiveSupport::Cache.lookup_store(store, *args)
63
+ end
64
+
65
+ def cache_store=(store)
66
+ @cache_store = store
67
+ end
68
+
69
+ def cache_store
70
+ @cache_store ||= JSONAPIonify.cache_store
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,15 @@
1
+ module JSONAPIonify::Api
2
+ module Base::Delegation
3
+
4
+ def self.extended(klass)
5
+ klass.class_eval do
6
+ class << self
7
+ delegate :context, :response_header, :helper, :rescue_from, :error,
8
+ :pagination, :before, :param, :request_header, :sorting,
9
+ to: :resource_class
10
+ end
11
+ end
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,47 @@
1
+ module JSONAPIonify::Api
2
+ module Base::DocHelper
3
+
4
+ def self.extended(klass)
5
+ klass.class_eval do
6
+ extend JSONAPIonify::InheritedAttributes
7
+ inherited_array_attribute :links
8
+ end
9
+ end
10
+
11
+ def link(href)
12
+ links << href
13
+ end
14
+
15
+ def title(title)
16
+ @title = title
17
+ end
18
+
19
+ def description(description)
20
+ @description = description
21
+ end
22
+
23
+ def documentation_output(request)
24
+ @documentation_output ||= JSONAPIonify::Documentation.new(documentation_object(request)).result
25
+ end
26
+
27
+ def resources_in_order
28
+ indexes = @documentation_order || []
29
+ resources.sort_by { |resource| indexes.index(resource.name) || indexes.length }
30
+ end
31
+
32
+ def documentation_object(request)
33
+ base_url = URI.parse(request.url).tap do |uri|
34
+ uri.query = nil
35
+ uri.path.chomp! request.path_info
36
+ uri.path.chomp! '/docs'
37
+ end.to_s
38
+ OpenStruct.new(
39
+ links: links,
40
+ title: @title || self.name,
41
+ base_url: base_url,
42
+ description: JSONAPIonify::Documentation.render_markdown(@description || ''),
43
+ resources: resources_in_order.map { |r| r.documentation_object base_url }
44
+ )
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,10 @@
1
+ module JSONAPIonify::Api
2
+ class Base::Reloader < Struct.new :app
3
+
4
+ def call(env)
5
+ Base.descendants.each(&:load_resources)
6
+ app.call(env)
7
+ end
8
+
9
+ end
10
+ end
@@ -0,0 +1,39 @@
1
+ module JSONAPIonify::Api
2
+ module Base::ResourceDefinitions
3
+
4
+ def self.extended(klass)
5
+ klass.class_eval do
6
+ extend JSONAPIonify::InheritedAttributes
7
+ inherited_hash_attribute :resource_definitions
8
+ end
9
+ end
10
+
11
+ def resource(type)
12
+ raise ArgumentError, 'type required' if type.nil?
13
+ type = type.to_sym
14
+ const_name = type.to_s.camelcase
15
+ return const_get(const_name, false) if const_defined?(const_name, false)
16
+ raise Errors::ResourceNotFound, "Resource not defined: #{type}" unless resource_defined?(type)
17
+ klass = Class.new(resource_class, &resource_definitions[type]).set_type(type)
18
+ param(:fields, type)
19
+ const_set const_name, klass
20
+ end
21
+
22
+ def resource_defined?(name)
23
+ !!resource_definitions[name]
24
+ end
25
+
26
+ def resources
27
+ resource_definitions.map do |name, _|
28
+ resource(name)
29
+ end
30
+ end
31
+
32
+ def define_resource(name, &block)
33
+ const_name = name.to_s.camelcase
34
+ remove_const(const_name) if const_defined? const_name
35
+ resource_definitions[name.to_sym] = block
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,25 @@
1
+ require 'active_support/descendants_tracker'
2
+ require 'active_support/core_ext/module/delegation'
3
+
4
+ module JSONAPIonify::Api
5
+ class Base
6
+ extend JSONAPIonify::Autoload
7
+ autoload_all
8
+ extend AppBuilder
9
+ extend DocHelper
10
+ extend ClassMethods
11
+ extend Delegation
12
+ extend ResourceDefinitions
13
+
14
+ def self.inherited(subclass)
15
+ super(subclass)
16
+ file = caller.reject { |f| f.start_with? JSONAPIonify.path }[0].split(/\:\d/)[0]
17
+ dir = File.expand_path File.dirname(file)
18
+ basename = File.basename(file, File.extname(file))
19
+ self.load_path = File.join(dir, basename)
20
+ subclass.const_set(:ResourceBase, Class.new(Resource).set_api(subclass))
21
+ load_resources
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ module JSONAPIonify::Api
2
+ class Context < Struct.new :block, :readonly
3
+
4
+ def call(instance, delegate)
5
+ block = self.block || proc {}
6
+ instance.instance_exec(delegate, &block)
7
+ end
8
+
9
+ def readonly?
10
+ !!readonly
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,42 @@
1
+ module JSONAPIonify::Api
2
+ class ContextDelegate
3
+ class Mock
4
+ def method_missing(*args, &block)
5
+ self
6
+ end
7
+ end
8
+
9
+ def initialize(request, instance, definitions)
10
+ memo = {}
11
+ delegate = self
12
+ @definitions = definitions
13
+
14
+ define_singleton_method :request do
15
+ request
16
+ end
17
+
18
+ %i{initialize_dup initialize_clone}.each do |method|
19
+ define_singleton_method method do |copy|
20
+ memo.each do |k, v|
21
+ copy.public_send "#{k}=", v
22
+ end
23
+ end
24
+ end
25
+
26
+ define_singleton_method(:reset) do |key|
27
+ memo.delete(key)
28
+ end
29
+
30
+ definitions.each do |name, context|
31
+ define_singleton_method name do
32
+ memo[name] ||= context.call(instance, delegate)
33
+ end
34
+
35
+ define_singleton_method "#{name}=" do |value|
36
+ memo[name] = value
37
+ end unless context.readonly?
38
+ end
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,6 @@
1
+ module JSONAPIonify::Api
2
+ module Errors
3
+ ResourceNotFound = Class.new StandardError
4
+ RelationshipNotFound = Class.new StandardError
5
+ end
6
+ end
@@ -0,0 +1,66 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+
3
+ module JSONAPIonify
4
+ module Api
5
+ class ErrorsObject
6
+
7
+ delegate :present?, to: :collection
8
+
9
+ class Evaluator
10
+ Structure::Objects::Error.permitted_keys.each do |key|
11
+ define_method(key) do |value|
12
+ @error[key] = value
13
+ end
14
+ end
15
+
16
+ def initialize(error)
17
+ @error = error
18
+ freeze
19
+ end
20
+
21
+ def meta
22
+ JSONAPIonify::Structure::Helpers::MetaDelegate.new @error
23
+ end
24
+
25
+ def pointer(value)
26
+ @error[:source] ||= {}
27
+ @error[:source][:pointer] = value
28
+ end
29
+
30
+ def parameter(value)
31
+ @error[:source] ||= {}
32
+ @error[:source][:parameter] = value
33
+ end
34
+ end
35
+
36
+ def evaluate(*args, error_block:, runtime_block:, backtrace: nil)
37
+ backtrace ||= caller
38
+ error = Structure::Objects::Error.new
39
+ evaluator = Evaluator.new(error)
40
+ collection << error
41
+ [runtime_block, error_block].each do |block|
42
+ evaluator.instance_exec(*args, &block) if block
43
+ end
44
+ unless ENV['RACK_ENV'] == 'production'
45
+ error[:meta] ||= {}
46
+ error[:meta][:backtrace] = backtrace
47
+ end
48
+ end
49
+
50
+ def top_level
51
+ JSONAPIonify.new_object.tap do |obj|
52
+ obj[:errors] = collection
53
+ end
54
+ end
55
+
56
+ def collection
57
+ @collection ||= Structure::Collections::Errors.new
58
+ end
59
+
60
+ def set(collection)
61
+ @collection = collection
62
+ end
63
+
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,13 @@
1
+ module JSONAPIonify::Api
2
+ class HeaderOptions
3
+
4
+ attr_reader :name, :actions, :required
5
+
6
+ def initialize(name, actions: nil, required: false)
7
+ @name = name
8
+ @actions = Array.wrap(actions)
9
+ @required = required
10
+ end
11
+
12
+ end
13
+ end