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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +29 -0
- data/.csslintrc +2 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +1171 -0
- data/.ruby-version +1 -0
- data/.travis.yml +10 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/Guardfile +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +34 -0
- data/TODO +13 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/config.ru +15 -0
- data/fixtures/documentation.json +364 -0
- data/jsonapionify.gemspec +50 -0
- data/lib/core_ext/boolean.rb +3 -0
- data/lib/jsonapionify/api/action.rb +211 -0
- data/lib/jsonapionify/api/attribute.rb +67 -0
- data/lib/jsonapionify/api/base/app_builder.rb +33 -0
- data/lib/jsonapionify/api/base/class_methods.rb +73 -0
- data/lib/jsonapionify/api/base/delegation.rb +15 -0
- data/lib/jsonapionify/api/base/doc_helper.rb +47 -0
- data/lib/jsonapionify/api/base/reloader.rb +10 -0
- data/lib/jsonapionify/api/base/resource_definitions.rb +39 -0
- data/lib/jsonapionify/api/base.rb +25 -0
- data/lib/jsonapionify/api/context.rb +14 -0
- data/lib/jsonapionify/api/context_delegate.rb +42 -0
- data/lib/jsonapionify/api/errors.rb +6 -0
- data/lib/jsonapionify/api/errors_object.rb +66 -0
- data/lib/jsonapionify/api/header_options.rb +13 -0
- data/lib/jsonapionify/api/param_options.rb +46 -0
- data/lib/jsonapionify/api/relationship/blocks.rb +41 -0
- data/lib/jsonapionify/api/relationship/many.rb +61 -0
- data/lib/jsonapionify/api/relationship/one.rb +36 -0
- data/lib/jsonapionify/api/relationship.rb +89 -0
- data/lib/jsonapionify/api/resource/builders.rb +81 -0
- data/lib/jsonapionify/api/resource/class_methods.rb +82 -0
- data/lib/jsonapionify/api/resource/defaults/actions.rb +11 -0
- data/lib/jsonapionify/api/resource/defaults/errors.rb +99 -0
- data/lib/jsonapionify/api/resource/defaults/request_contexts.rb +96 -0
- data/lib/jsonapionify/api/resource/defaults/response_contexts.rb +31 -0
- data/lib/jsonapionify/api/resource/defaults.rb +10 -0
- data/lib/jsonapionify/api/resource/definitions/actions.rb +196 -0
- data/lib/jsonapionify/api/resource/definitions/attributes.rb +51 -0
- data/lib/jsonapionify/api/resource/definitions/contexts.rb +16 -0
- data/lib/jsonapionify/api/resource/definitions/helpers.rb +9 -0
- data/lib/jsonapionify/api/resource/definitions/pagination.rb +79 -0
- data/lib/jsonapionify/api/resource/definitions/params.rb +49 -0
- data/lib/jsonapionify/api/resource/definitions/relationships.rb +42 -0
- data/lib/jsonapionify/api/resource/definitions/request_headers.rb +103 -0
- data/lib/jsonapionify/api/resource/definitions/response_headers.rb +22 -0
- data/lib/jsonapionify/api/resource/definitions/scopes.rb +50 -0
- data/lib/jsonapionify/api/resource/definitions/sorting.rb +85 -0
- data/lib/jsonapionify/api/resource/definitions.rb +14 -0
- data/lib/jsonapionify/api/resource/error_handling.rb +108 -0
- data/lib/jsonapionify/api/resource/http.rb +11 -0
- data/lib/jsonapionify/api/resource/includer.rb +4 -0
- data/lib/jsonapionify/api/resource.rb +35 -0
- data/lib/jsonapionify/api/response.rb +47 -0
- data/lib/jsonapionify/api/server/mock_response.rb +37 -0
- data/lib/jsonapionify/api/server/request.rb +78 -0
- data/lib/jsonapionify/api/server.rb +50 -0
- data/lib/jsonapionify/api/test_helper.rb +52 -0
- data/lib/jsonapionify/api.rb +9 -0
- data/lib/jsonapionify/autoload.rb +52 -0
- data/lib/jsonapionify/callbacks.rb +49 -0
- data/lib/jsonapionify/character_range.rb +41 -0
- data/lib/jsonapionify/continuation.rb +26 -0
- data/lib/jsonapionify/documentation/template.erb +487 -0
- data/lib/jsonapionify/documentation.rb +40 -0
- data/lib/jsonapionify/enumerable_observer.rb +91 -0
- data/lib/jsonapionify/indented_string.rb +27 -0
- data/lib/jsonapionify/inherited_attributes.rb +125 -0
- data/lib/jsonapionify/structure/collections/base.rb +104 -0
- data/lib/jsonapionify/structure/collections/errors.rb +7 -0
- data/lib/jsonapionify/structure/collections/included_resources.rb +39 -0
- data/lib/jsonapionify/structure/collections/resource_identifiers.rb +7 -0
- data/lib/jsonapionify/structure/collections/resources.rb +7 -0
- data/lib/jsonapionify/structure/helpers/errors.rb +71 -0
- data/lib/jsonapionify/structure/helpers/inherits_origin.rb +17 -0
- data/lib/jsonapionify/structure/helpers/member_names.rb +37 -0
- data/lib/jsonapionify/structure/helpers/meta_delegate.rb +16 -0
- data/lib/jsonapionify/structure/helpers/object_defaults.rb +123 -0
- data/lib/jsonapionify/structure/helpers/object_setters.rb +21 -0
- data/lib/jsonapionify/structure/helpers/pagination_links.rb +10 -0
- data/lib/jsonapionify/structure/helpers/validations.rb +296 -0
- data/lib/jsonapionify/structure/maps/base.rb +25 -0
- data/lib/jsonapionify/structure/maps/error_links.rb +7 -0
- data/lib/jsonapionify/structure/maps/links.rb +21 -0
- data/lib/jsonapionify/structure/maps/relationship_links.rb +11 -0
- data/lib/jsonapionify/structure/maps/relationships.rb +23 -0
- data/lib/jsonapionify/structure/maps/resource_links.rb +7 -0
- data/lib/jsonapionify/structure/maps/top_level_links.rb +10 -0
- data/lib/jsonapionify/structure/objects/attributes.rb +29 -0
- data/lib/jsonapionify/structure/objects/base.rb +166 -0
- data/lib/jsonapionify/structure/objects/error.rb +16 -0
- data/lib/jsonapionify/structure/objects/included_resource.rb +14 -0
- data/lib/jsonapionify/structure/objects/jsonapi.rb +7 -0
- data/lib/jsonapionify/structure/objects/link.rb +18 -0
- data/lib/jsonapionify/structure/objects/meta.rb +7 -0
- data/lib/jsonapionify/structure/objects/relationship.rb +20 -0
- data/lib/jsonapionify/structure/objects/resource.rb +45 -0
- data/lib/jsonapionify/structure/objects/resource_identifier.rb +40 -0
- data/lib/jsonapionify/structure/objects/source.rb +10 -0
- data/lib/jsonapionify/structure/objects/top_level.rb +105 -0
- data/lib/jsonapionify/structure.rb +27 -0
- data/lib/jsonapionify/types/array_type.rb +32 -0
- data/lib/jsonapionify/types/boolean_type.rb +22 -0
- data/lib/jsonapionify/types/date_string_type.rb +28 -0
- data/lib/jsonapionify/types/float_type.rb +8 -0
- data/lib/jsonapionify/types/integer_type.rb +9 -0
- data/lib/jsonapionify/types/object_type.rb +22 -0
- data/lib/jsonapionify/types/string_type.rb +66 -0
- data/lib/jsonapionify/types/time_string_type.rb +28 -0
- data/lib/jsonapionify/types.rb +49 -0
- data/lib/jsonapionify/unstrict_proc.rb +28 -0
- data/lib/jsonapionify/version.rb +3 -0
- data/lib/jsonapionify.rb +37 -0
- metadata +530 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
module Resource::Definitions::Sorting
|
|
3
|
+
|
|
4
|
+
class SortField
|
|
5
|
+
|
|
6
|
+
attr_reader :name, :order
|
|
7
|
+
|
|
8
|
+
def initialize(name)
|
|
9
|
+
if name.to_s.start_with? '-'
|
|
10
|
+
@name = name.to_s[1..-1].to_sym
|
|
11
|
+
@order = :desc
|
|
12
|
+
else
|
|
13
|
+
@name = name
|
|
14
|
+
@order = :asc
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
STRATEGIES = {
|
|
21
|
+
active_record: proc { |collection, fields|
|
|
22
|
+
order_hash = fields.each_with_object({}) do |field, hash|
|
|
23
|
+
hash[field.name] = field.order
|
|
24
|
+
end
|
|
25
|
+
collection.order order_hash
|
|
26
|
+
},
|
|
27
|
+
enumerable: proc { |collection, fields|
|
|
28
|
+
fields.reverse.reduce(collection) do |o, field|
|
|
29
|
+
result = o.sort_by(&field.name)
|
|
30
|
+
case field.order
|
|
31
|
+
when :asc
|
|
32
|
+
result
|
|
33
|
+
when :desc
|
|
34
|
+
result.reverse
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
STRATEGIES[:array] = STRATEGIES[:enumerable]
|
|
40
|
+
DEFAULT = STRATEGIES[:enumerable]
|
|
41
|
+
|
|
42
|
+
def self.extended(klass)
|
|
43
|
+
klass.class_eval do
|
|
44
|
+
|
|
45
|
+
context(:sort_params, readonly: true) do |context|
|
|
46
|
+
should_error = false
|
|
47
|
+
fields = context.params['sort'].to_s.split(',')
|
|
48
|
+
fields.each_with_object([]) do |field, array|
|
|
49
|
+
field, resource = field.split('.').map(&:to_sym).reverse
|
|
50
|
+
if self.class <= self.class.api.resource(resource || self.class.type)
|
|
51
|
+
if self.class.field_valid? field
|
|
52
|
+
array << SortField.new(field)
|
|
53
|
+
else
|
|
54
|
+
should_error = true
|
|
55
|
+
error :sort_parameter_invalid do
|
|
56
|
+
detail "resource `#{self.class.type}` does not have field: #{field}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end.tap do
|
|
61
|
+
raise error_exception if should_error
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def sorting(strategy = nil, &block)
|
|
68
|
+
param :sort
|
|
69
|
+
context :sorted_collection do |context|
|
|
70
|
+
unless (actual_block = block)
|
|
71
|
+
actual_strategy = strategy || self.class.default_strategy
|
|
72
|
+
actual_block = actual_strategy ? STRATEGIES[actual_strategy] : DEFAULT
|
|
73
|
+
end
|
|
74
|
+
Object.new.instance_exec(context.collection, context.sort_params, &actual_block)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def default_sort
|
|
81
|
+
api.resource_definitions.keys.each_with_object({}) { |type, h| h[type] = [] }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
module Resource::Definitions
|
|
3
|
+
extend JSONAPIonify::Autoload
|
|
4
|
+
autoload_all
|
|
5
|
+
|
|
6
|
+
def self.extended(klass)
|
|
7
|
+
klass.extend Contexts
|
|
8
|
+
constants(false).each do |const|
|
|
9
|
+
mod = const_get(const, false)
|
|
10
|
+
klass.extend mod unless klass.singleton_class < mod
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
module Resource::ErrorHandling
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
include ActiveSupport::Rescuable
|
|
7
|
+
context(:error_exception) { Class.new(StandardError) }
|
|
8
|
+
context(:errors, readonly: true) do
|
|
9
|
+
ErrorsObject.new
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module ClassMethods
|
|
14
|
+
|
|
15
|
+
def error(name, &block)
|
|
16
|
+
self.error_definitions = self.error_definitions.merge name.to_sym => block
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def rescue_from(*klasses, error:, &block)
|
|
20
|
+
super(*klasses) do |exception|
|
|
21
|
+
errors.evaluate(
|
|
22
|
+
error_block: lookup_error(error),
|
|
23
|
+
runtime_block: block || proc {},
|
|
24
|
+
backtrace: exception.backtrace
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def error_definitions=(hash)
|
|
30
|
+
@error_definitions = hash
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def error_definitions
|
|
34
|
+
@error_definitions ||= {}
|
|
35
|
+
if superclass.respond_to?(:error_definitions)
|
|
36
|
+
superclass.error_definitions.merge(@error_definitions)
|
|
37
|
+
else
|
|
38
|
+
@error_definitions
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def error(name, *args, &block)
|
|
44
|
+
errors.evaluate(
|
|
45
|
+
*args,
|
|
46
|
+
error_block: lookup_error(name),
|
|
47
|
+
runtime_block: block
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def error_now(name, *args, &block)
|
|
52
|
+
error(name, *args, &block)
|
|
53
|
+
raise error_exception
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def set_errors(collection)
|
|
57
|
+
errors.set collection
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def error_meta
|
|
61
|
+
errors.meta
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def lookup_error(name)
|
|
67
|
+
self.class.error_definitions[name].tap do |error|
|
|
68
|
+
raise ArgumentError, "Error does not exist: #{name}" unless error
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def rescued_response(exception)
|
|
73
|
+
rescue_with_handler(exception) || begin
|
|
74
|
+
errors.evaluate(
|
|
75
|
+
error_block: lookup_error(:internal_server_error),
|
|
76
|
+
runtime_block: proc {
|
|
77
|
+
unless ENV['RACK_ENV'] == 'production'
|
|
78
|
+
detail exception.message
|
|
79
|
+
meta[:error_class] = exception.class.name
|
|
80
|
+
end
|
|
81
|
+
},
|
|
82
|
+
backtrace: exception.backtrace
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
ensure
|
|
86
|
+
return error_response
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def error_response
|
|
90
|
+
Rack::Response.new.tap do |response|
|
|
91
|
+
error_collection = errors.collection
|
|
92
|
+
status_codes = error_collection.map { |error| error[:status] }.compact.uniq.sort
|
|
93
|
+
response.status =
|
|
94
|
+
if status_codes.length == 1
|
|
95
|
+
status_codes[0].to_i
|
|
96
|
+
elsif status_codes.blank?
|
|
97
|
+
500
|
|
98
|
+
else
|
|
99
|
+
(status_codes.last[0] + "00").to_i
|
|
100
|
+
end
|
|
101
|
+
response_headers.each { |k, v| response.headers[k] = v }
|
|
102
|
+
response.headers['content-type'] = 'application/vnd.api+json'
|
|
103
|
+
response.write(errors.top_level.to_json)
|
|
104
|
+
end.finish
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require 'active_support/rescuable'
|
|
2
|
+
require 'rack/response'
|
|
3
|
+
require 'active_support/json'
|
|
4
|
+
|
|
5
|
+
module JSONAPIonify::Api
|
|
6
|
+
class Resource
|
|
7
|
+
extend JSONAPIonify::Autoload
|
|
8
|
+
autoload_all
|
|
9
|
+
|
|
10
|
+
extend Definitions
|
|
11
|
+
extend ClassMethods
|
|
12
|
+
|
|
13
|
+
include ErrorHandling
|
|
14
|
+
include Builders
|
|
15
|
+
include Defaults
|
|
16
|
+
|
|
17
|
+
def self.inherited(subclass)
|
|
18
|
+
super(subclass)
|
|
19
|
+
subclass.class_eval do
|
|
20
|
+
context(:api, readonly: true) { api }
|
|
21
|
+
context(:resource, readonly: true) { self }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.example_instance(id=1)
|
|
26
|
+
OpenStruct.new.tap do |instance|
|
|
27
|
+
instance.send "#{id_attribute}=", (id).to_s
|
|
28
|
+
attributes.select(&:read?).each do |attribute|
|
|
29
|
+
instance.send "#{attribute.name}=", attribute.example
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
class Response
|
|
3
|
+
attr_reader :action, :accept, :response_block, :status
|
|
4
|
+
|
|
5
|
+
def initialize(action, accept: nil, status: nil, &block)
|
|
6
|
+
@action = action
|
|
7
|
+
@response_block = block || proc {}
|
|
8
|
+
@accept = accept || 'application/vnd.api+json'
|
|
9
|
+
@status = status || 200
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def ==(other)
|
|
13
|
+
self.class == other.class &&
|
|
14
|
+
%i{@accept}.all? do |ivar|
|
|
15
|
+
instance_variable_get(ivar) == other.instance_variable_get(ivar)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def accept?(request)
|
|
20
|
+
request.accept.any? do |accept|
|
|
21
|
+
@accept == accept || accept == '*/*' || self.accept == '*/*'
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def documentation_object
|
|
26
|
+
OpenStruct.new(
|
|
27
|
+
accept: accept,
|
|
28
|
+
content_type: accept,
|
|
29
|
+
status: status
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def call(instance, context)
|
|
34
|
+
response = self
|
|
35
|
+
instance.instance_eval do
|
|
36
|
+
body = instance_exec(context, &response.response_block)
|
|
37
|
+
Rack::Response.new.tap do |rack_response|
|
|
38
|
+
rack_response.status = response.status
|
|
39
|
+
response_headers.each { |k, v| rack_response.headers[k] = v }
|
|
40
|
+
rack_response.headers['content-type'] = response.accept unless response.accept == '*/*'
|
|
41
|
+
rack_response.write(body) unless body.nil?
|
|
42
|
+
end.finish
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require 'rack/utils'
|
|
2
|
+
|
|
3
|
+
module JSONAPIonify::Api
|
|
4
|
+
class Server::MockResponse
|
|
5
|
+
attr_reader :status, :headers, :body
|
|
6
|
+
|
|
7
|
+
def initialize(status, headers, body)
|
|
8
|
+
@status = status
|
|
9
|
+
@body = body.is_a?(Rack::BodyProxy) ? body.body : body
|
|
10
|
+
@headers = Rack::Utils::HeaderHash.new headers
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def body
|
|
14
|
+
return nil unless @body.present?
|
|
15
|
+
JSON.pretty_generate(Oj.load(@body.join("\n")))
|
|
16
|
+
rescue Oj::ParseError
|
|
17
|
+
@body
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def http_string
|
|
21
|
+
# HTTP/1.1 200 OK
|
|
22
|
+
# Date: Fri, 31 Dec 1999 23:59:59 GMT
|
|
23
|
+
# Content-Type: text/html
|
|
24
|
+
# Content-Length: 1354
|
|
25
|
+
#
|
|
26
|
+
# <body>
|
|
27
|
+
[].tap do |lines|
|
|
28
|
+
lines << "HTTP/1.1 #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]}"
|
|
29
|
+
headers.each do |k, v|
|
|
30
|
+
lines << "#{k.split('-').map(&:capitalize).join('-')}: #{v}"
|
|
31
|
+
end
|
|
32
|
+
lines << ''
|
|
33
|
+
lines << body if body.present?
|
|
34
|
+
end.join("\n")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require 'rack/request'
|
|
2
|
+
require 'rack/utils'
|
|
3
|
+
require 'rack/mock'
|
|
4
|
+
|
|
5
|
+
module JSONAPIonify::Api
|
|
6
|
+
class Server::Request < Rack::Request
|
|
7
|
+
def self.env_for(url, method, options = {})
|
|
8
|
+
options[:method] = method
|
|
9
|
+
new Rack::MockRequest.env_for(url, options)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def headers
|
|
13
|
+
Rack::Utils::HeaderHash.new(
|
|
14
|
+
env.select do |name, _|
|
|
15
|
+
name.start_with?('HTTP_') && !%w{HTTP_VERSION}.include?(name)
|
|
16
|
+
end.each_with_object({}) do |(name, value), hash|
|
|
17
|
+
hash[name[5..-1].gsub('_', '-')] = value
|
|
18
|
+
end
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def http_string
|
|
23
|
+
# GET /path/file.html HTTP/1.0
|
|
24
|
+
# From: someuser@jmarshall.com
|
|
25
|
+
# User-Agent: HTTPTool/1.0
|
|
26
|
+
# [blank line here]
|
|
27
|
+
[].tap do |lines|
|
|
28
|
+
lines << "#{request_method} #{fullpath} HTTP/1.1"
|
|
29
|
+
headers.each do |k, v|
|
|
30
|
+
lines << "#{k.split('-').map(&:capitalize).join('-')}: #{v}"
|
|
31
|
+
end
|
|
32
|
+
lines << ''
|
|
33
|
+
lines << pretty_body if has_body?
|
|
34
|
+
end.join("\n")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def pretty_body
|
|
38
|
+
return '' unless has_body?
|
|
39
|
+
value = body.read
|
|
40
|
+
JSON.pretty_generate(Oj.load(value))
|
|
41
|
+
rescue Oj::ParseError
|
|
42
|
+
value
|
|
43
|
+
ensure
|
|
44
|
+
body.rewind
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def accept
|
|
48
|
+
accepts = (headers['accept'] || '*/*').split(',')
|
|
49
|
+
accepts.to_a.sort_by! do |accept|
|
|
50
|
+
_, *media_type_params = accept.split(';')
|
|
51
|
+
rqf = media_type_params.find { |mtp| mtp.start_with? 'q=' }
|
|
52
|
+
-(rqf ? rqf[2..-1].to_f : 1.0)
|
|
53
|
+
end.map do |accept|
|
|
54
|
+
mime, *media_type_params = accept.split(';')
|
|
55
|
+
media_type_params.reject! { |mtp| mtp.start_with? 'q=' }
|
|
56
|
+
[mime, *media_type_params].join(';')
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def authorizations
|
|
61
|
+
parts = headers['authorization'].to_s.split(' ')
|
|
62
|
+
parts.length == 2 ? Rack::Utils::HeaderHash.new([parts].to_h) : {}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def has_body?
|
|
66
|
+
body.read(1).present?
|
|
67
|
+
ensure
|
|
68
|
+
body.rewind
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def root_url
|
|
72
|
+
URI.parse(url).tap do |uri|
|
|
73
|
+
uri.query = nil
|
|
74
|
+
uri.path.chomp! path_info
|
|
75
|
+
end.to_s
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require 'delegate'
|
|
2
|
+
require 'active_support/core_ext/object/blank'
|
|
3
|
+
|
|
4
|
+
module JSONAPIonify::Api
|
|
5
|
+
class Server
|
|
6
|
+
extend JSONAPIonify::Autoload
|
|
7
|
+
autoload_all
|
|
8
|
+
|
|
9
|
+
attr_reader :api
|
|
10
|
+
|
|
11
|
+
def initialize(api)
|
|
12
|
+
@api = api
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(env)
|
|
16
|
+
Processor.new(env, api).response
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class Processor
|
|
20
|
+
attr_reader :request, :id, :more, :api
|
|
21
|
+
|
|
22
|
+
def initialize(env, api)
|
|
23
|
+
@api = api
|
|
24
|
+
@request = Request.new(env)
|
|
25
|
+
request.path_info.split('/').tap(&:shift).tap do |parts|
|
|
26
|
+
@resource, @id, @relationship, @relationship_name, *@more = parts
|
|
27
|
+
request.env['jsonapionify.resource_name'] = @resource if @resource
|
|
28
|
+
request.env['jsonapionify.resource'] = resource if @resource
|
|
29
|
+
request.env['jsonapionify.id'] = @id if @id
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def response
|
|
34
|
+
@resource ? resource.process(request) : api_index
|
|
35
|
+
rescue Errors::ResourceNotFound
|
|
36
|
+
Resource::Http.process(:not_found, request)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def api_index
|
|
42
|
+
api.process_index(request)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def resource
|
|
46
|
+
api.resource(@resource)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require 'rack/test'
|
|
2
|
+
require 'active_support/concern'
|
|
3
|
+
|
|
4
|
+
module JSONAPIonify
|
|
5
|
+
module Api::TestHelper
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
include Rack::Test::Methods
|
|
8
|
+
|
|
9
|
+
module ClassMethods
|
|
10
|
+
def set_api(api)
|
|
11
|
+
define_method(:app) do
|
|
12
|
+
api
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def set_headers
|
|
18
|
+
@set_headers ||= Rack::Utils::HeaderHash.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def json(hash)
|
|
22
|
+
Oj.dump hash.deep_stringify_keys
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def last_response_json
|
|
26
|
+
Oj.load last_response.body
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def header(name, value)
|
|
30
|
+
set_headers[name] = value
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def content_type(value)
|
|
35
|
+
header('content-type', value)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def accept(*values)
|
|
39
|
+
header('accept', values.join(','))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def delete(*args, &block)
|
|
43
|
+
header('content-type', set_headers['content-type'].to_s)
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def authorization(type, value)
|
|
48
|
+
header 'Authorization', [type, value].join(' ')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require 'active_support/inflections'
|
|
2
|
+
|
|
3
|
+
module JSONAPIonify
|
|
4
|
+
module Autoload
|
|
5
|
+
def self.eager_load!
|
|
6
|
+
load_tracker = {}
|
|
7
|
+
while unloaded.present?
|
|
8
|
+
unloaded.each do |mod, consts|
|
|
9
|
+
consts.each do |const|
|
|
10
|
+
tracker = [mod.name, const].join('::')
|
|
11
|
+
begin
|
|
12
|
+
mod.const_get(const, false)
|
|
13
|
+
rescue NameError => e
|
|
14
|
+
load_tracker[tracker] = load_tracker[tracker].to_i + 1
|
|
15
|
+
if !e.message.include?('uninitialized constant') || load_tracker[tracker] > 50
|
|
16
|
+
raise e
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.unloaded
|
|
25
|
+
modules = ObjectSpace.each_object.select do |o|
|
|
26
|
+
o.is_a?(Module)
|
|
27
|
+
end
|
|
28
|
+
modules.each_with_object({}) do |mod, hash|
|
|
29
|
+
autoloadable_constants = mod.constants.each_with_object([]) do |const, ary|
|
|
30
|
+
if mod.autoload?(const) && mod.autoload?(const).include?(__dir__)
|
|
31
|
+
ary << const
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
if autoloadable_constants.present?
|
|
35
|
+
hash[mod] = autoloadable_constants
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def autoload_all(dir=nil)
|
|
41
|
+
file = caller[0].split(/\:\d/)[0]
|
|
42
|
+
base_dir = File.expand_path File.dirname(file)
|
|
43
|
+
dir ||= name.split('::').last.underscore
|
|
44
|
+
Dir.glob("#{base_dir}/#{dir}/*.rb").each do |file|
|
|
45
|
+
basename = File.basename file, File.extname(file)
|
|
46
|
+
fullpath = File.expand_path file
|
|
47
|
+
const_name = basename.camelize.to_sym
|
|
48
|
+
autoload const_name, fullpath
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require 'active_support/concern'
|
|
2
|
+
|
|
3
|
+
module JSONAPIonify
|
|
4
|
+
module Callbacks
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
included do
|
|
7
|
+
|
|
8
|
+
def self.define_callbacks(*names)
|
|
9
|
+
names.each do |name|
|
|
10
|
+
chains = {
|
|
11
|
+
main: "__#{name}_callback_chain",
|
|
12
|
+
before: "__#{name}_before_callback_chain",
|
|
13
|
+
after: "__#{name}_after_callback_chain"
|
|
14
|
+
}
|
|
15
|
+
define_method chains[:main] do |*args, &block|
|
|
16
|
+
if send(chains[:before], *args) != false
|
|
17
|
+
value = instance_exec(*args, &block)
|
|
18
|
+
value if send(chains[:after], *args) != false
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Define before and after chains
|
|
23
|
+
%i{after before}.each do |timing|
|
|
24
|
+
define_method chains[timing] do |*|
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
define_singleton_method "#{timing}_#{name}" do |sym = nil, &outer_block|
|
|
28
|
+
outer_block = (outer_block || sym).to_proc
|
|
29
|
+
prev_chain = instance_method(chains[timing])
|
|
30
|
+
define_method chains[timing] do |*args, &block|
|
|
31
|
+
if prev_chain.bind(self).call(*args, &block) != false
|
|
32
|
+
instance_exec(*args, &outer_block)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private *chains.values
|
|
39
|
+
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def run_callbacks(name, *args, &block)
|
|
45
|
+
send("__#{name}_callback_chain", *args, &block)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module JSONAPIonify
|
|
2
|
+
class CharacterRange
|
|
3
|
+
include Enumerable
|
|
4
|
+
|
|
5
|
+
class << self
|
|
6
|
+
def [](*args)
|
|
7
|
+
new(*args)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(start_char, end_char)
|
|
12
|
+
if [start_char, end_char].any? { |c| c.length > 1 }
|
|
13
|
+
raise ArgumentError, 'must be single characters'
|
|
14
|
+
end
|
|
15
|
+
@start_char = start_char
|
|
16
|
+
@end_char = end_char
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def each
|
|
20
|
+
range.each do |ord|
|
|
21
|
+
begin
|
|
22
|
+
char = ord.chr(Encoding::UTF_8)
|
|
23
|
+
yield char
|
|
24
|
+
rescue RangeError
|
|
25
|
+
next
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_a
|
|
31
|
+
each.to_a
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def range
|
|
37
|
+
(@start_char.ord)..(@end_char.ord)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
end
|
|
41
|
+
end
|