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,46 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
class ParamOptions
|
|
3
|
+
|
|
4
|
+
def self.hash_to_keypaths(hash)
|
|
5
|
+
mapper = lambda do |hash, ary|
|
|
6
|
+
hash.each_with_object(ary) do |(k, v), a|
|
|
7
|
+
a << (map = [k.to_sym])
|
|
8
|
+
mapper[v, map] if v.is_a?(Hash)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
mapper[hash, []].map(&:flatten)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.keypath_to_string(*paths)
|
|
15
|
+
first_path, *rest = paths
|
|
16
|
+
"#{first_path}#{rest.map { |path| "[#{path}]" }.join}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.invalid_parameters(hash, keypaths)
|
|
20
|
+
invalid_key_paths = hash_to_keypaths(hash) - keypaths
|
|
21
|
+
invalid_key_paths.map do |paths|
|
|
22
|
+
keypath_to_string(*paths)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.missing_parameters(hash, keypaths)
|
|
27
|
+
missing_key_paths = keypaths - hash_to_keypaths(hash)
|
|
28
|
+
missing_key_paths.map do |paths|
|
|
29
|
+
keypath_to_string(*paths)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
attr_reader :keypath, :actions, :required
|
|
34
|
+
|
|
35
|
+
def initialize(*keys, actions: nil, required: false)
|
|
36
|
+
@keypath = keys
|
|
37
|
+
@actions = Array.wrap(actions)
|
|
38
|
+
@required = required
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def string
|
|
42
|
+
self.class.keypath_to_string(*@keypath)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
module Relationship::Blocks
|
|
3
|
+
|
|
4
|
+
def self.extended(klass)
|
|
5
|
+
klass.class_eval do
|
|
6
|
+
prepend_class
|
|
7
|
+
append_class
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def prepend_class(&block)
|
|
12
|
+
block ||= proc {}
|
|
13
|
+
if method_defined? :class_prepends
|
|
14
|
+
previous_prepends = instance_method(:class_prepends)
|
|
15
|
+
define_method :class_prepends do
|
|
16
|
+
(previous_prepends.bind(self).call + [block]).freeze
|
|
17
|
+
end
|
|
18
|
+
else
|
|
19
|
+
prepends = [block].freeze
|
|
20
|
+
define_method :class_prepends do
|
|
21
|
+
prepends
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def append_class(&block)
|
|
27
|
+
block ||= proc {}
|
|
28
|
+
if method_defined? :class_appends
|
|
29
|
+
previous_appends = instance_method(:class_appends)
|
|
30
|
+
define_method :class_appends do
|
|
31
|
+
(previous_appends.bind(self).call + [block]).freeze
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
appends = [block].freeze
|
|
35
|
+
define_method :class_appends do
|
|
36
|
+
appends
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
class Relationship::Many < Relationship
|
|
3
|
+
|
|
4
|
+
prepend_class do
|
|
5
|
+
rel = self.rel
|
|
6
|
+
remove_action :read
|
|
7
|
+
class << self
|
|
8
|
+
undef_method :read
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
define_singleton_method(:show) do |**options, &block|
|
|
12
|
+
options[:prepend] = 'relationships'
|
|
13
|
+
define_action(:show, 'GET', **options, &block).response status: 200 do |context|
|
|
14
|
+
context.response_object[:data] = build_identifier_collection(context.collection)
|
|
15
|
+
context.meta[:total_count] = context.collection.count
|
|
16
|
+
context.response_object.to_json
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
define_singleton_method(:replace) do |**options, &block|
|
|
21
|
+
options[:prepend] = 'relationships'
|
|
22
|
+
define_action(:replace, 'PATCH', '', true, :resource_identifier, **options, &block).response status: 200 do |context|
|
|
23
|
+
context.owner_context.reset(:instance)
|
|
24
|
+
context.reset(:collection)
|
|
25
|
+
context.response_object[:data] = build_identifier_collection(context.collection)
|
|
26
|
+
context.meta[:total_count] = context.collection.count
|
|
27
|
+
context.response_object.to_json
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
define_singleton_method(:add) do |**options, &block|
|
|
32
|
+
options[:prepend] = 'relationships'
|
|
33
|
+
define_action(:add, 'POST', '', true, :resource_identifier, **options, &block).response status: 200 do |context|
|
|
34
|
+
context.owner_context.reset(:instance)
|
|
35
|
+
context.reset(:collection)
|
|
36
|
+
context.response_object[:data] = build_identifier_collection(context.collection)
|
|
37
|
+
context.meta[:total_count] = context.collection.count
|
|
38
|
+
context.response_object.to_json
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
define_singleton_method(:remove) do |**options, &block|
|
|
43
|
+
options[:prepend] = 'relationships'
|
|
44
|
+
define_action(:remove, 'DELETE', '', true, :resource_identifier, **options, &block).response status: 200 do |context|
|
|
45
|
+
context.owner_context.reset(:instance)
|
|
46
|
+
context.reset(:collection)
|
|
47
|
+
context.response_object[:data] = build_identifier_collection(context.collection)
|
|
48
|
+
context.meta[:total_count] = context.collection.count
|
|
49
|
+
context.response_object.to_json
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
context :scope do |context|
|
|
54
|
+
context.owner_context.instance.send(rel.name)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
show
|
|
58
|
+
replace { error_now :forbidden }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
class Relationship::One < Relationship
|
|
3
|
+
|
|
4
|
+
prepend_class do
|
|
5
|
+
rel = self.rel
|
|
6
|
+
remove_action :list, :create
|
|
7
|
+
class << self
|
|
8
|
+
undef_method :list, :create
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
define_singleton_method(:show) do |**options, &block|
|
|
12
|
+
options[:prepend] = 'relationships'
|
|
13
|
+
define_action(:show, 'GET', '', nil, :resource_identifier, **options, &block).response status: 200 do |context|
|
|
14
|
+
context.response_object[:data] = build_resource_identifier(context.instance)
|
|
15
|
+
context.response_object.to_json
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
define_singleton_method(:replace) do |**options, &block|
|
|
20
|
+
options[:prepend] = 'relationships'
|
|
21
|
+
define_action(:replace, 'PATCH', '', nil, :resource_identifier, **options, &block).response status: 200 do |context|
|
|
22
|
+
context.owner_context.reset(:instance)
|
|
23
|
+
context.reset(:instance)
|
|
24
|
+
context.response_object[:data] = build_resource_identifier(context.instance)
|
|
25
|
+
context.response_object.to_json
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
context :instance do |context|
|
|
30
|
+
context.owner_context.instance.send(rel.name)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
show
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
class Relationship
|
|
3
|
+
extend JSONAPIonify::Autoload
|
|
4
|
+
autoload_all
|
|
5
|
+
|
|
6
|
+
extend Blocks
|
|
7
|
+
|
|
8
|
+
prepend_class do
|
|
9
|
+
remove_action :delete, :update
|
|
10
|
+
class << self
|
|
11
|
+
undef_method :delete, :update
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
append_class do
|
|
16
|
+
|
|
17
|
+
def self.supports_path?
|
|
18
|
+
false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.relationship(name)
|
|
22
|
+
rel.resource.relationship(name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
owner_context_proc = Proc.new do |request|
|
|
26
|
+
ContextDelegate.new(request, rel.owner.new, rel.owner.context_definitions)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
context(:owner_context) do |context|
|
|
30
|
+
owner_context_proc.call(context.request)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
context(:id) do
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
define_singleton_method :base_path do
|
|
38
|
+
"/#{rel.owner.type}/:id"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
define_singleton_method :path_name do
|
|
42
|
+
rel.name.to_s
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
define_singleton_method(:build_links) do |base_url|
|
|
46
|
+
JSONAPIonify::Structure::Maps::RelationshipLinks.new(
|
|
47
|
+
self: File.join(base_url, 'relationships', rel.name.to_s),
|
|
48
|
+
related: File.join(base_url, rel.name.to_s)
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
attr_reader :owner, :class_proc, :name
|
|
54
|
+
|
|
55
|
+
def initialize(owner, name, resource: nil, &block)
|
|
56
|
+
@class_proc = block || proc {}
|
|
57
|
+
@owner = owner
|
|
58
|
+
@name = name
|
|
59
|
+
@resource = resource || name
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def documentation_object(base_url)
|
|
63
|
+
OpenStruct.new(
|
|
64
|
+
name: name,
|
|
65
|
+
actions: resource_class.actions.map { |a| a.documentation_object resource_class, base_url, name.to_s, false }
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def resource_class
|
|
70
|
+
@resource_class ||= begin
|
|
71
|
+
rel = self
|
|
72
|
+
Class.new(resource) do
|
|
73
|
+
define_singleton_method(:rel) do
|
|
74
|
+
rel
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
rel.class_prepends.each { |prepend| class_eval &prepend }
|
|
78
|
+
class_eval(&rel.class_proc)
|
|
79
|
+
rel.class_appends.each { |append| class_eval &append }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def resource
|
|
85
|
+
owner.api.resource(@resource)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
module Resource::Builders
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
module ClassMethods
|
|
6
|
+
def build_resource(request, instance, fields: api.fields, relationships: true, links: true)
|
|
7
|
+
return nil unless instance
|
|
8
|
+
resource_url = build_url(request, instance)
|
|
9
|
+
id = build_id(instance)
|
|
10
|
+
JSONAPIonify::Structure::Objects::Resource.new.tap do |resource|
|
|
11
|
+
resource[:id] = id
|
|
12
|
+
resource[:type] = type
|
|
13
|
+
resource[:attributes] = fields[type.to_sym].each_with_object(JSONAPIonify::Structure::Objects::Attributes.new) do |member, attributes|
|
|
14
|
+
attributes[member.to_sym] = instance.public_send(member)
|
|
15
|
+
end
|
|
16
|
+
resource[:links] = JSONAPIonify::Structure::Objects::Links.new(
|
|
17
|
+
self: resource_url
|
|
18
|
+
) if links
|
|
19
|
+
resource[:relationships] = relationship_definitions.each_with_object(JSONAPIonify::Structure::Maps::Relationships.new) do |rel, hash|
|
|
20
|
+
hash[rel.name] = build_relationship(request, instance, rel.name)
|
|
21
|
+
end if relationships
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def build_resource_identifier(instance)
|
|
26
|
+
JSONAPIonify::Structure::Objects::ResourceIdentifier.new(
|
|
27
|
+
id: build_id(instance),
|
|
28
|
+
type: type.to_s
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def build_collection(request, collection, fields: api.fields, relationships: false)
|
|
33
|
+
collection.each_with_object(JSONAPIonify::Structure::Collections::Resources.new) do |instance, resources|
|
|
34
|
+
resources << build_resource(request, instance, fields: fields, relationships: relationships)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def build_identifier_collection(collection)
|
|
39
|
+
collection.each_with_object(JSONAPIonify::Structure::Collections::ResourceIdentifiers.new) do |instance, resource_identifiers|
|
|
40
|
+
resource_identifiers << build_resource_identifier(instance)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def build_relationship(request, instance, name, links: true, data: false)
|
|
45
|
+
resource_url = build_url(request, instance)
|
|
46
|
+
relationship = self.relationship(name)
|
|
47
|
+
JSONAPIonify::Structure::Objects::Relationship.new.tap do |rel|
|
|
48
|
+
rel[:links] = relationship.build_links(resource_url) if links
|
|
49
|
+
if data
|
|
50
|
+
rel[:data] =
|
|
51
|
+
if relationship < Resource::RelationshipToMany
|
|
52
|
+
instance.send(name).map do |child|
|
|
53
|
+
relationship.build_resource_identifier(child)
|
|
54
|
+
end
|
|
55
|
+
elsif relationship < Resource::RelationshipToOne
|
|
56
|
+
value = instance.send(name)
|
|
57
|
+
relationship.build_resource_identifier value if value
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build_url(request, instance = nil)
|
|
64
|
+
if instance
|
|
65
|
+
File.join(request.root_url, type, build_id(instance))
|
|
66
|
+
else
|
|
67
|
+
File.join(request.root_url, type)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def build_id(instance)
|
|
72
|
+
instance.send(id_attribute).to_s
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
included do
|
|
77
|
+
delegate *(ClassMethods.instance_methods - JSONAPIonify::Api::Resource::Builders.instance_methods), to: :class
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
require 'active_support/rescuable'
|
|
2
|
+
|
|
3
|
+
module JSONAPIonify::Api
|
|
4
|
+
module Resource::ClassMethods
|
|
5
|
+
|
|
6
|
+
def self.extended(klass)
|
|
7
|
+
klass.include ActiveSupport::Rescuable
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def description(description)
|
|
11
|
+
@description = description
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def set_api(api)
|
|
15
|
+
self.tap do
|
|
16
|
+
define_singleton_method :api do
|
|
17
|
+
api
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def set_type(type)
|
|
23
|
+
type = type.to_s
|
|
24
|
+
self.tap do
|
|
25
|
+
define_singleton_method :type do
|
|
26
|
+
type
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def type
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def api
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def get_url(base)
|
|
40
|
+
File.join base, type.to_s
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def actions_in_order
|
|
44
|
+
indexes = %i{list create read update delete add replace remove}
|
|
45
|
+
actions.sort_by { |action| indexes.index(action.name) || indexes.length }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def documentation_object(base_url)
|
|
49
|
+
url = File.join(base_url, type)
|
|
50
|
+
OpenStruct.new(
|
|
51
|
+
name: type,
|
|
52
|
+
description: JSONAPIonify::Documentation.render_markdown(@description || ''),
|
|
53
|
+
relationships: relationships.map { |r| r.documentation_object url },
|
|
54
|
+
attributes: attributes.map(&:documentation_object),
|
|
55
|
+
actions: actions_in_order.map { |a| a.documentation_object self, base_url, type, true }
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def cache(store, *args)
|
|
60
|
+
self.cache_store = ActiveSupport::Cache.lookup_store(store, *args)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def cache_store=(store)
|
|
64
|
+
@cache_store = store
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def cache_store
|
|
68
|
+
@cache_store ||= api.cache_store
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def default_strategy
|
|
72
|
+
if defined?(ActiveRecord) && current_scope.is_a?(Class) && current_scope < ActiveRecord::Base
|
|
73
|
+
:active_record
|
|
74
|
+
elsif Enumerable === current_scope || (current_scope.is_a?(Class) && current_scope < Enumerable)
|
|
75
|
+
:enumerable
|
|
76
|
+
end
|
|
77
|
+
rescue NotImplementedError
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
require 'oj'
|
|
2
|
+
|
|
3
|
+
module JSONAPIonify::Api
|
|
4
|
+
module Resource::Defaults::Errors
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
rescue_from JSONAPIonify::Structure::ValidationError, error: :jsonapi_validation_error
|
|
9
|
+
rescue_from Oj::ParseError, error: :json_parse_error
|
|
10
|
+
|
|
11
|
+
Rack::Utils::SYMBOL_TO_STATUS_CODE.each do |symbol, code|
|
|
12
|
+
message = Rack::Utils::HTTP_STATUS_CODES[code]
|
|
13
|
+
error symbol do
|
|
14
|
+
title message
|
|
15
|
+
status code.to_s
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
error :data_missing do
|
|
20
|
+
pointer ''
|
|
21
|
+
title 'Missing Member'
|
|
22
|
+
detail 'missing data member'
|
|
23
|
+
status '422'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
error :json_parse_error do
|
|
27
|
+
title 'Parse Error'
|
|
28
|
+
detail 'Could not parse JSON object'
|
|
29
|
+
status '422'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
error :field_not_permitted do |type, field|
|
|
33
|
+
parameter "fields[#{type}]"
|
|
34
|
+
title 'Invalid Field'
|
|
35
|
+
detail "type: `#{type}`, does not have field: `#{field}`"
|
|
36
|
+
status '400'
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
error :attribute_not_permitted do |attribute|
|
|
40
|
+
pointer "data/attributes/#{attribute}"
|
|
41
|
+
title 'Attribute not permitted'
|
|
42
|
+
detail "Attribute not permitted: #{attribute}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
error :attributes_missing do
|
|
46
|
+
pointer 'data'
|
|
47
|
+
title 'Missing Member'
|
|
48
|
+
detail 'missing attributes member'
|
|
49
|
+
status '422'
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
error :include_parameter_invalid do
|
|
53
|
+
parameter 'sort'
|
|
54
|
+
title 'Sort parameter is invalid'
|
|
55
|
+
status '400'
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
error :parameters_missing do |parameters|
|
|
59
|
+
title 'Missing required parameters'
|
|
60
|
+
detail "missing: #{parameters.to_sentence}"
|
|
61
|
+
status '400'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
error :parameter_not_permitted do |param|
|
|
65
|
+
parameter param
|
|
66
|
+
title 'Parameter Not Permitted'
|
|
67
|
+
detail "parameter not permitted: #{param}"
|
|
68
|
+
status '400'
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
error :headers_missing do |headers|
|
|
72
|
+
title 'Missing required headers'
|
|
73
|
+
detail "missing: #{headers.to_sentence}"
|
|
74
|
+
status '400'
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
error :header_not_permitted do |header|
|
|
78
|
+
title 'Header Not Permitted'
|
|
79
|
+
detail "header not permitted: #{header}"
|
|
80
|
+
status '400'
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
error :sort_parameter_invalid do
|
|
84
|
+
parameter 'sort'
|
|
85
|
+
title 'Sort parameter is invalid'
|
|
86
|
+
status '400'
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
error :request_object_invalid do |context, request_object|
|
|
90
|
+
context.errors.set request_object.errors.as_collection
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
error :resource_invalid do
|
|
94
|
+
title 'Invalid Resource'
|
|
95
|
+
status '404'
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
module Resource::Defaults::RequestContexts
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
context(:request_body, readonly: true) do |context|
|
|
7
|
+
context.request.body.read
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
context(:request_object, readonly: true) do |context|
|
|
11
|
+
JSONAPIonify.parse(context.request_body).as(:client).tap do |input|
|
|
12
|
+
error_now(:request_object_invalid, context, input) unless input.validate
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
context(:id, readonly: true) do |context|
|
|
17
|
+
context.request.env['jsonapionify.id']
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
context(:request_attributes, readonly: true) do |context|
|
|
21
|
+
request_object = context.request_object
|
|
22
|
+
request_attributes = context.request_data.fetch(:attributes) do
|
|
23
|
+
error_now :attributes_missing
|
|
24
|
+
end
|
|
25
|
+
request_attributes.tap do |attributes|
|
|
26
|
+
writable_attributes = context.request_resource.attributes.select(&:write?)
|
|
27
|
+
required_attributes = writable_attributes.select(&:required?).map(&:name)
|
|
28
|
+
optional_attributes = writable_attributes.select(&:optional?).map(&:name)
|
|
29
|
+
if (extra_attributes = attributes.keys - (optional_attributes + required_attributes)).present?
|
|
30
|
+
extra_attributes.each { |attr| error :attribute_not_permitted, attr }
|
|
31
|
+
raise error_exception
|
|
32
|
+
end
|
|
33
|
+
request_object.validate
|
|
34
|
+
error_now(:request_object_invalid, context, request_object) if request_object.errors.present?
|
|
35
|
+
end.to_hash
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
context(:request_instances, readonly: true) do |context|
|
|
39
|
+
should_error = false
|
|
40
|
+
data = context.request_data
|
|
41
|
+
instances = data.map.each_with_index do |item, i|
|
|
42
|
+
begin
|
|
43
|
+
find_instance item, pointer: "data/#{i}"
|
|
44
|
+
rescue error_exception
|
|
45
|
+
should_error = true
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
raise error_exception if should_error
|
|
49
|
+
instances
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
context(:request_instance, readonly: true) do |context|
|
|
53
|
+
find_instance(context.request_data, pointer: 'data')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
context(:request_resource, readonly: true) do |context|
|
|
57
|
+
item = context.request_data
|
|
58
|
+
find_resource item, pointer: 'data'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
context(:request_data) do |context|
|
|
62
|
+
context.request_object.fetch(:data) {
|
|
63
|
+
error_now(:data_missing)
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def find_instance(item, pointer:)
|
|
69
|
+
should_error = false
|
|
70
|
+
resource = find_resource(item, pointer: pointer)
|
|
71
|
+
unless (instance = resource.find_instance item[:id])
|
|
72
|
+
should_error = true
|
|
73
|
+
error :resource_invalid do
|
|
74
|
+
self.pointer pointer
|
|
75
|
+
self.detail "could not find resource: `#{item[:type]}` with id: #{item[:id]}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
raise error_exception if should_error
|
|
79
|
+
instance
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def find_resource(item, pointer:)
|
|
83
|
+
should_error = false
|
|
84
|
+
unless (resource = self.class.api.resource item[:type])
|
|
85
|
+
should_error = true
|
|
86
|
+
error :resource_invalid do
|
|
87
|
+
self.pointer pointer
|
|
88
|
+
self.detail "could not find resource: `#{item[:type]}`"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
raise error_exception if should_error
|
|
92
|
+
resource
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
module Resource::Defaults::ResponseContexts
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
|
|
7
|
+
# Response Objects
|
|
8
|
+
context(:links, readonly: true) do |context|
|
|
9
|
+
context.response_object[:links]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
context(:meta, readonly: true) do |context|
|
|
13
|
+
JSONAPIonify::Structure::Helpers::MetaDelegate.new context.response_object
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
context(:response_object) do |context|
|
|
17
|
+
JSONAPIonify.parse(links: { self: context.request.url })
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
context(:response_collection) do |context|
|
|
21
|
+
collections = %i{
|
|
22
|
+
paginated_collection
|
|
23
|
+
sorted_collection
|
|
24
|
+
collection
|
|
25
|
+
}
|
|
26
|
+
context.public_send collections.find { |c| context.respond_to? c }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|