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