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