jsonapionify 0.0.1.pre

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