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.
Files changed (124) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +29 -0
  3. data/.csslintrc +2 -0
  4. data/.gitignore +11 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +1171 -0
  7. data/.ruby-version +1 -0
  8. data/.travis.yml +10 -0
  9. data/CODE_OF_CONDUCT.md +13 -0
  10. data/Gemfile +4 -0
  11. data/Guardfile +14 -0
  12. data/LICENSE.txt +21 -0
  13. data/README.md +43 -0
  14. data/Rakefile +34 -0
  15. data/TODO +13 -0
  16. data/bin/console +14 -0
  17. data/bin/setup +7 -0
  18. data/config.ru +15 -0
  19. data/fixtures/documentation.json +364 -0
  20. data/jsonapionify.gemspec +50 -0
  21. data/lib/core_ext/boolean.rb +3 -0
  22. data/lib/jsonapionify/api/action.rb +211 -0
  23. data/lib/jsonapionify/api/attribute.rb +67 -0
  24. data/lib/jsonapionify/api/base/app_builder.rb +33 -0
  25. data/lib/jsonapionify/api/base/class_methods.rb +73 -0
  26. data/lib/jsonapionify/api/base/delegation.rb +15 -0
  27. data/lib/jsonapionify/api/base/doc_helper.rb +47 -0
  28. data/lib/jsonapionify/api/base/reloader.rb +10 -0
  29. data/lib/jsonapionify/api/base/resource_definitions.rb +39 -0
  30. data/lib/jsonapionify/api/base.rb +25 -0
  31. data/lib/jsonapionify/api/context.rb +14 -0
  32. data/lib/jsonapionify/api/context_delegate.rb +42 -0
  33. data/lib/jsonapionify/api/errors.rb +6 -0
  34. data/lib/jsonapionify/api/errors_object.rb +66 -0
  35. data/lib/jsonapionify/api/header_options.rb +13 -0
  36. data/lib/jsonapionify/api/param_options.rb +46 -0
  37. data/lib/jsonapionify/api/relationship/blocks.rb +41 -0
  38. data/lib/jsonapionify/api/relationship/many.rb +61 -0
  39. data/lib/jsonapionify/api/relationship/one.rb +36 -0
  40. data/lib/jsonapionify/api/relationship.rb +89 -0
  41. data/lib/jsonapionify/api/resource/builders.rb +81 -0
  42. data/lib/jsonapionify/api/resource/class_methods.rb +82 -0
  43. data/lib/jsonapionify/api/resource/defaults/actions.rb +11 -0
  44. data/lib/jsonapionify/api/resource/defaults/errors.rb +99 -0
  45. data/lib/jsonapionify/api/resource/defaults/request_contexts.rb +96 -0
  46. data/lib/jsonapionify/api/resource/defaults/response_contexts.rb +31 -0
  47. data/lib/jsonapionify/api/resource/defaults.rb +10 -0
  48. data/lib/jsonapionify/api/resource/definitions/actions.rb +196 -0
  49. data/lib/jsonapionify/api/resource/definitions/attributes.rb +51 -0
  50. data/lib/jsonapionify/api/resource/definitions/contexts.rb +16 -0
  51. data/lib/jsonapionify/api/resource/definitions/helpers.rb +9 -0
  52. data/lib/jsonapionify/api/resource/definitions/pagination.rb +79 -0
  53. data/lib/jsonapionify/api/resource/definitions/params.rb +49 -0
  54. data/lib/jsonapionify/api/resource/definitions/relationships.rb +42 -0
  55. data/lib/jsonapionify/api/resource/definitions/request_headers.rb +103 -0
  56. data/lib/jsonapionify/api/resource/definitions/response_headers.rb +22 -0
  57. data/lib/jsonapionify/api/resource/definitions/scopes.rb +50 -0
  58. data/lib/jsonapionify/api/resource/definitions/sorting.rb +85 -0
  59. data/lib/jsonapionify/api/resource/definitions.rb +14 -0
  60. data/lib/jsonapionify/api/resource/error_handling.rb +108 -0
  61. data/lib/jsonapionify/api/resource/http.rb +11 -0
  62. data/lib/jsonapionify/api/resource/includer.rb +4 -0
  63. data/lib/jsonapionify/api/resource.rb +35 -0
  64. data/lib/jsonapionify/api/response.rb +47 -0
  65. data/lib/jsonapionify/api/server/mock_response.rb +37 -0
  66. data/lib/jsonapionify/api/server/request.rb +78 -0
  67. data/lib/jsonapionify/api/server.rb +50 -0
  68. data/lib/jsonapionify/api/test_helper.rb +52 -0
  69. data/lib/jsonapionify/api.rb +9 -0
  70. data/lib/jsonapionify/autoload.rb +52 -0
  71. data/lib/jsonapionify/callbacks.rb +49 -0
  72. data/lib/jsonapionify/character_range.rb +41 -0
  73. data/lib/jsonapionify/continuation.rb +26 -0
  74. data/lib/jsonapionify/documentation/template.erb +487 -0
  75. data/lib/jsonapionify/documentation.rb +40 -0
  76. data/lib/jsonapionify/enumerable_observer.rb +91 -0
  77. data/lib/jsonapionify/indented_string.rb +27 -0
  78. data/lib/jsonapionify/inherited_attributes.rb +125 -0
  79. data/lib/jsonapionify/structure/collections/base.rb +104 -0
  80. data/lib/jsonapionify/structure/collections/errors.rb +7 -0
  81. data/lib/jsonapionify/structure/collections/included_resources.rb +39 -0
  82. data/lib/jsonapionify/structure/collections/resource_identifiers.rb +7 -0
  83. data/lib/jsonapionify/structure/collections/resources.rb +7 -0
  84. data/lib/jsonapionify/structure/helpers/errors.rb +71 -0
  85. data/lib/jsonapionify/structure/helpers/inherits_origin.rb +17 -0
  86. data/lib/jsonapionify/structure/helpers/member_names.rb +37 -0
  87. data/lib/jsonapionify/structure/helpers/meta_delegate.rb +16 -0
  88. data/lib/jsonapionify/structure/helpers/object_defaults.rb +123 -0
  89. data/lib/jsonapionify/structure/helpers/object_setters.rb +21 -0
  90. data/lib/jsonapionify/structure/helpers/pagination_links.rb +10 -0
  91. data/lib/jsonapionify/structure/helpers/validations.rb +296 -0
  92. data/lib/jsonapionify/structure/maps/base.rb +25 -0
  93. data/lib/jsonapionify/structure/maps/error_links.rb +7 -0
  94. data/lib/jsonapionify/structure/maps/links.rb +21 -0
  95. data/lib/jsonapionify/structure/maps/relationship_links.rb +11 -0
  96. data/lib/jsonapionify/structure/maps/relationships.rb +23 -0
  97. data/lib/jsonapionify/structure/maps/resource_links.rb +7 -0
  98. data/lib/jsonapionify/structure/maps/top_level_links.rb +10 -0
  99. data/lib/jsonapionify/structure/objects/attributes.rb +29 -0
  100. data/lib/jsonapionify/structure/objects/base.rb +166 -0
  101. data/lib/jsonapionify/structure/objects/error.rb +16 -0
  102. data/lib/jsonapionify/structure/objects/included_resource.rb +14 -0
  103. data/lib/jsonapionify/structure/objects/jsonapi.rb +7 -0
  104. data/lib/jsonapionify/structure/objects/link.rb +18 -0
  105. data/lib/jsonapionify/structure/objects/meta.rb +7 -0
  106. data/lib/jsonapionify/structure/objects/relationship.rb +20 -0
  107. data/lib/jsonapionify/structure/objects/resource.rb +45 -0
  108. data/lib/jsonapionify/structure/objects/resource_identifier.rb +40 -0
  109. data/lib/jsonapionify/structure/objects/source.rb +10 -0
  110. data/lib/jsonapionify/structure/objects/top_level.rb +105 -0
  111. data/lib/jsonapionify/structure.rb +27 -0
  112. data/lib/jsonapionify/types/array_type.rb +32 -0
  113. data/lib/jsonapionify/types/boolean_type.rb +22 -0
  114. data/lib/jsonapionify/types/date_string_type.rb +28 -0
  115. data/lib/jsonapionify/types/float_type.rb +8 -0
  116. data/lib/jsonapionify/types/integer_type.rb +9 -0
  117. data/lib/jsonapionify/types/object_type.rb +22 -0
  118. data/lib/jsonapionify/types/string_type.rb +66 -0
  119. data/lib/jsonapionify/types/time_string_type.rb +28 -0
  120. data/lib/jsonapionify/types.rb +49 -0
  121. data/lib/jsonapionify/unstrict_proc.rb +28 -0
  122. data/lib/jsonapionify/version.rb +3 -0
  123. data/lib/jsonapionify.rb +37 -0
  124. metadata +530 -0
@@ -0,0 +1,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,11 @@
1
+ module JSONAPIonify::Api
2
+ module Resource::Defaults::Actions
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ before(:create) { |context| context.instance = context.new_instance }
7
+ list
8
+ read
9
+ end
10
+ end
11
+ 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
@@ -0,0 +1,10 @@
1
+ module JSONAPIonify::Api
2
+ module Resource::Defaults
3
+ extend JSONAPIonify::Autoload
4
+ autoload_all
5
+
6
+ def self.included(klass)
7
+ constants(false).each { |const| klass.include const_get const, false }
8
+ end
9
+ end
10
+ end