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