fun_with_json_api 0.0.1

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 (76) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Rakefile +28 -0
  4. data/config/locales/fun_with_json_api.en.yml +13 -0
  5. data/lib/fun_with_json_api/attribute.rb +38 -0
  6. data/lib/fun_with_json_api/attributes/boolean_attribute.rb +24 -0
  7. data/lib/fun_with_json_api/attributes/date_attribute.rb +22 -0
  8. data/lib/fun_with_json_api/attributes/datetime_attribute.rb +20 -0
  9. data/lib/fun_with_json_api/attributes/decimal_attribute.rb +23 -0
  10. data/lib/fun_with_json_api/attributes/float_attribute.rb +20 -0
  11. data/lib/fun_with_json_api/attributes/integer_attribute.rb +20 -0
  12. data/lib/fun_with_json_api/attributes/relationship.rb +73 -0
  13. data/lib/fun_with_json_api/attributes/relationship_collection.rb +99 -0
  14. data/lib/fun_with_json_api/attributes/string_attribute.rb +9 -0
  15. data/lib/fun_with_json_api/controller_methods.rb +12 -0
  16. data/lib/fun_with_json_api/deserializer.rb +70 -0
  17. data/lib/fun_with_json_api/deserializer_class_methods.rb +83 -0
  18. data/lib/fun_with_json_api/deserializer_config_builder.rb +48 -0
  19. data/lib/fun_with_json_api/exception.rb +27 -0
  20. data/lib/fun_with_json_api/exception_payload.rb +29 -0
  21. data/lib/fun_with_json_api/exception_payload_serializer.rb +17 -0
  22. data/lib/fun_with_json_api/exception_serializer.rb +15 -0
  23. data/lib/fun_with_json_api/exceptions/invalid_attribute.rb +13 -0
  24. data/lib/fun_with_json_api/exceptions/invalid_document.rb +12 -0
  25. data/lib/fun_with_json_api/exceptions/invalid_relationship.rb +13 -0
  26. data/lib/fun_with_json_api/exceptions/missing_relationship.rb +15 -0
  27. data/lib/fun_with_json_api/pre_deserializer.rb +61 -0
  28. data/lib/fun_with_json_api/railtie.rb +11 -0
  29. data/lib/fun_with_json_api/version.rb +3 -0
  30. data/lib/fun_with_json_api.rb +24 -0
  31. data/lib/tasks/fun_with_json_api_tasks.rake +4 -0
  32. data/spec/dummy/README.rdoc +28 -0
  33. data/spec/dummy/Rakefile +6 -0
  34. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  35. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  36. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  37. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  38. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  39. data/spec/dummy/bin/bundle +3 -0
  40. data/spec/dummy/bin/rails +4 -0
  41. data/spec/dummy/bin/rake +4 -0
  42. data/spec/dummy/bin/setup +29 -0
  43. data/spec/dummy/config/application.rb +25 -0
  44. data/spec/dummy/config/boot.rb +5 -0
  45. data/spec/dummy/config/database.yml +25 -0
  46. data/spec/dummy/config/environment.rb +5 -0
  47. data/spec/dummy/config/environments/development.rb +41 -0
  48. data/spec/dummy/config/environments/production.rb +80 -0
  49. data/spec/dummy/config/environments/test.rb +42 -0
  50. data/spec/dummy/config/initializers/assets.rb +11 -0
  51. data/spec/dummy/config/initializers/backtrace_silencers.rb +9 -0
  52. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  53. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  54. data/spec/dummy/config/initializers/inflections.rb +16 -0
  55. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  56. data/spec/dummy/config/initializers/session_store.rb +3 -0
  57. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  58. data/spec/dummy/config/locales/en.yml +23 -0
  59. data/spec/dummy/config/routes.rb +56 -0
  60. data/spec/dummy/config/secrets.yml +22 -0
  61. data/spec/dummy/config.ru +4 -0
  62. data/spec/dummy/log/test.log +37839 -0
  63. data/spec/dummy/public/404.html +67 -0
  64. data/spec/dummy/public/422.html +67 -0
  65. data/spec/dummy/public/500.html +66 -0
  66. data/spec/dummy/public/favicon.ico +0 -0
  67. data/spec/example_spec.rb +64 -0
  68. data/spec/fixtures/active_record.rb +65 -0
  69. data/spec/fun_with_json_api/controller_methods_spec.rb +123 -0
  70. data/spec/fun_with_json_api/deserializer_class_methods_spec.rb +52 -0
  71. data/spec/fun_with_json_api/deserializer_spec.rb +450 -0
  72. data/spec/fun_with_json_api/exception_spec.rb +77 -0
  73. data/spec/fun_with_json_api/pre_deserializer_spec.rb +287 -0
  74. data/spec/fun_with_json_api_spec.rb +55 -0
  75. data/spec/spec_helper.rb +33 -0
  76. metadata +275 -0
@@ -0,0 +1,70 @@
1
+ require 'active_support/inflector'
2
+ require 'fun_with_json_api/attribute'
3
+ require 'fun_with_json_api/deserializer_class_methods'
4
+
5
+ module FunWithJsonApi
6
+ class Deserializer
7
+ extend FunWithJsonApi::DeserializerClassMethods
8
+
9
+ # Creates a new instance of a
10
+ def self.create(options = {})
11
+ new(options)
12
+ end
13
+
14
+ # Use DeserializerClass.create to build new instances
15
+ private_class_method :new
16
+
17
+ attr_reader :id_param
18
+ attr_reader :type
19
+ attr_reader :resource_class
20
+
21
+ attr_reader :attributes
22
+ attr_reader :relationships
23
+
24
+ def initialize(options = {})
25
+ @id_param = options.fetch(:id_param) { self.class.id_param }
26
+ @type = options[:type]
27
+ @resource_class = options[:resource_class]
28
+ @attributes = filter_attributes_by_name(options[:attributes], self.class.attributes)
29
+ @relationships = filter_attributes_by_name(options[:relationships], self.class.relationships)
30
+ end
31
+
32
+ # Takes a parsed params hash from ActiveModelSerializers::Deserialization and sanitizes values
33
+ def sanitize_params(params)
34
+ Hash[
35
+ serialize_attribute_values(attributes, params) +
36
+ serialize_attribute_values(relationships, params)
37
+ ]
38
+ end
39
+
40
+ def type
41
+ @type ||= self.class.type
42
+ end
43
+
44
+ def resource_class
45
+ @resource_class ||= self.class.resource_class
46
+ end
47
+
48
+ private
49
+
50
+ def filter_attributes_by_name(attribute_names, attributes)
51
+ if attribute_names
52
+ attributes.keep_if { |attribute| attribute_names.include?(attribute.name) }
53
+ else
54
+ attributes
55
+ end
56
+ end
57
+
58
+ # Calls <attribute.as> on the current instance, override the #<as> method to change loading
59
+ def serialize_attribute_values(attributes, params)
60
+ attributes.select { |attribute| params.key?(attribute.param_value) }
61
+ .map { |attribute| serialize_attribute(attribute, params) }
62
+ end
63
+
64
+ # Calls <attribute.as> on the current instance, override the #<as> method to change loading
65
+ def serialize_attribute(attribute, params)
66
+ raw_value = params.fetch(attribute.param_value)
67
+ [attribute.param_value, public_send(attribute.sanitize_attribute_method, raw_value)]
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,83 @@
1
+ require 'fun_with_json_api/attribute'
2
+
3
+ module FunWithJsonApi
4
+ # Provides a basic DSL for defining a FunWithJsonApi::Deserializer
5
+ module DeserializerClassMethods
6
+ def id_param(id_param = nil)
7
+ @id_param = id_param if id_param
8
+ @id_param || :id
9
+ end
10
+
11
+ def type(type = nil)
12
+ @type = type if type
13
+ @type || type_from_class_name
14
+ end
15
+
16
+ def resource_class(resource_class = nil)
17
+ @resource_class = resource_class if resource_class
18
+ @resource_class || type_from_class_name.singularize.classify.constantize
19
+ end
20
+
21
+ # Attributes
22
+
23
+ def attribute(name, options = {})
24
+ Attribute.create(name, options).tap do |attribute|
25
+ add_parse_attribute_method(attribute)
26
+ attributes << attribute
27
+ end
28
+ end
29
+
30
+ def attributes
31
+ @attributes ||= []
32
+ end
33
+
34
+ # Relationships
35
+
36
+ def belongs_to(name, deserializer_class_or_callable, options = {})
37
+ Attributes::Relationship.create(
38
+ name,
39
+ deserializer_class_or_callable,
40
+ options
41
+ ).tap do |relationship|
42
+ add_parse_attribute_method(relationship)
43
+ relationships << relationship
44
+ end
45
+ end
46
+
47
+ # rubocop:disable Style/PredicateName
48
+
49
+ def has_many(name, deserializer_class_or_callable, options = {})
50
+ Attributes::RelationshipCollection.create(
51
+ name,
52
+ deserializer_class_or_callable,
53
+ options
54
+ ).tap do |relationship|
55
+ add_parse_attribute_method(relationship)
56
+ relationships << relationship
57
+ end
58
+ end
59
+
60
+ # rubocop:enable Style/PredicateName
61
+
62
+ def relationships
63
+ @relationships ||= []
64
+ end
65
+
66
+ private
67
+
68
+ def add_parse_attribute_method(attribute)
69
+ define_method(attribute.sanitize_attribute_method) do |param_value|
70
+ attribute.call(param_value)
71
+ end
72
+ end
73
+
74
+ def type_from_class_name
75
+ resource_class_name = name.demodulize.sub(/Deserializer/, '').underscore
76
+ if ActiveModelSerializers.config.jsonapi_resource_type == :singular
77
+ resource_class_name.singularize
78
+ else
79
+ resource_class_name.pluralize
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,48 @@
1
+ module FunWithJsonAPi
2
+ # Builds an options hash for ActiveModelSerializers::Deserialization.jsonapi_parse
3
+ class DeserializerConfigBuilder
4
+ def self.build(deserializer)
5
+ new(deserializer).build
6
+ end
7
+
8
+ private_class_method :new
9
+
10
+ attr_reader :deserializer
11
+
12
+ def initialize(deserializer)
13
+ @deserializer = deserializer
14
+ end
15
+
16
+ def build
17
+ {
18
+ only: build_only_values,
19
+ keys: build_keys_value
20
+ }
21
+ end
22
+
23
+ protected
24
+
25
+ def build_only_values
26
+ attribute_only_values(deserializer.attributes) +
27
+ attribute_only_values(deserializer.relationships)
28
+ end
29
+
30
+ def build_keys_value
31
+ Hash[
32
+ attribute_key_values(deserializer.attributes) +
33
+ attribute_key_values(deserializer.relationships)
34
+ ]
35
+ end
36
+
37
+ private
38
+
39
+ def attribute_only_values(attributes_or_relationships)
40
+ attributes_or_relationships.map(&:name)
41
+ end
42
+
43
+ def attribute_key_values(attributes_or_relationships)
44
+ attributes_or_relationships.select { |a| a.name != a.as }
45
+ .map { |a| [a.name, a.as] }
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,27 @@
1
+ require 'fun_with_json_api/exception_payload'
2
+
3
+ module FunWithJsonApi
4
+ class Exception < StandardError
5
+ attr_reader :payload
6
+
7
+ def initialize(message, payload)
8
+ super(message)
9
+ @payload = Array.wrap(payload)
10
+ end
11
+
12
+ # @return [Integer] The http status code for rendering this error
13
+ def http_status
14
+ payload_statuses = payload.map(&:status).uniq
15
+ if payload_statuses.length == 1
16
+ Integer(payload_statuses.first || '400') # Return the unique status code
17
+ elsif payload_statuses.any? { |status| status.starts_with?('5') }
18
+ 500 # We have a server error
19
+ else
20
+ 400 # Bad Request
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ # Load known Exceptions
27
+ Dir["#{File.dirname(__FILE__)}/exceptions/**/*.rb"].each { |f| require f }
@@ -0,0 +1,29 @@
1
+ require 'active_model_serializers/model'
2
+
3
+ module FunWithJsonApi
4
+ # id: a unique identifier for this particular occurrence of the problem.
5
+ # links: a links object containing the following members:
6
+ # about: a link that leads to further details about this particular occurrence of the problem.
7
+ # status: the HTTP status code applicable to this problem, expressed as a string value.
8
+ # code: an application-specific error code, expressed as a string value.
9
+ # title: a short, human-readable summary of the problem that SHOULD NOT change from occurrence to
10
+ # occurrence of the problem, except for purposes of localization.
11
+ # detail: a human-readable explanation specific to this occurrence of the problem. Like title,
12
+ # this field's value can be localized.
13
+ # source: an object containing references to the source of the error, optionally including any of
14
+ # the following members:
15
+ # pointer: a JSON Pointer [RFC6901] to the associated entity in the request document
16
+ # [e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute].
17
+ # parameter: a string indicating which URI query parameter caused the error.
18
+ # meta: a meta object containing non-standard meta-information about the error.
19
+ class ExceptionPayload < ActiveModelSerializers::Model
20
+ [:id, :status, :code, :title, :detail, :pointer, :parameter].each do |param|
21
+ define_method param do
22
+ attributes[param]
23
+ end
24
+ define_method "#{param}=" do |value|
25
+ attributes[param] = value
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,17 @@
1
+ module FunWithJsonApi
2
+ class ExceptionPayloadSerializer < ActiveModel::Serializer
3
+ attributes :id, :status, :code, :title, :detail, :source
4
+
5
+ def attributes(*)
6
+ # Strips all empty values and empty arrays
7
+ super.select { |_k, v| v.present? }
8
+ end
9
+
10
+ def source
11
+ {
12
+ pointer: object.pointer,
13
+ parameter: object.parameter
14
+ }.select { |_k, v| v.present? }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ require 'fun_with_json_api/exception_payload_serializer'
2
+
3
+ module FunWithJsonApi
4
+ class ExceptionSerializer < ActiveModel::Serializer::CollectionSerializer
5
+ def initialize(exception, options = {})
6
+ super(exception.payload, options.reverse_merge(
7
+ serializer: ExceptionPayloadSerializer
8
+ ))
9
+ end
10
+
11
+ def root
12
+ 'errors'
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ module FunWithJsonApi
2
+ module Exceptions
3
+ # Indicates a Supplied attributes value is not formatted correctly
4
+ class InvalidAttribute < FunWithJsonApi::Exception
5
+ def initialize(message, payload = ExceptionPayload.new)
6
+ payload.code ||= 'invalid_attribute'
7
+ payload.title ||= I18n.t(:invalid_attribute, scope: 'fun_with_json_api.exceptions')
8
+ payload.status ||= '400'
9
+ super
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ module FunWithJsonApi
2
+ module Exceptions
3
+ class InvalidDocument < FunWithJsonApi::Exception
4
+ def initialize(message, payload = ExceptionPayload.new)
5
+ payload.code ||= 'invalid_document'
6
+ payload.title ||= I18n.t(:invalid_document, scope: 'fun_with_json_api.exceptions')
7
+ payload.status ||= '400'
8
+ super
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ module FunWithJsonApi
2
+ module Exceptions
3
+ # Indicates a Supplied relationships value is not formatted correctly
4
+ class InvalidRelationship < FunWithJsonApi::Exception
5
+ def initialize(message, payload = ExceptionPayload.new)
6
+ payload.code ||= 'invalid_relationship'
7
+ payload.title ||= I18n.t(:invalid_relationship, scope: 'fun_with_json_api.exceptions')
8
+ payload.status ||= '400'
9
+ super
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ module FunWithJsonApi
2
+ module Exceptions
3
+ # Indicates a Supplied relationships value is not able to be found
4
+ class MissingRelationship < FunWithJsonApi::Exception
5
+ def initialize(message, payload = ExceptionPayload.new)
6
+ payload = Array.wrap(payload).each do |missing|
7
+ missing.code ||= 'missing_relationship'
8
+ missing.title ||= I18n.t(:missing_relationship, scope: 'fun_with_json_api.exceptions')
9
+ missing.status ||= '404'
10
+ end
11
+ super
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,61 @@
1
+ require 'active_model_serializers'
2
+
3
+ module FunWithJsonApi
4
+ # Converts a json_api document into a rails compatible hash.
5
+ # Acts as an adaptor for ActiveModuleSerializer deserializer classes
6
+ class PreDeserializer
7
+ def self.parse(document, deserializer)
8
+ new(document, deserializer).parse
9
+ end
10
+
11
+ attr_reader :document
12
+ attr_reader :deserializer
13
+
14
+ def initialize(document, deserializer)
15
+ @document = document.to_h.deep_dup.deep_stringify_keys
16
+ @deserializer = deserializer
17
+ end
18
+
19
+ def parse
20
+ ams_deserializer_class.parse(document, ams_deserializer_config) do |invalid_document, reason|
21
+ exception_message = "Invalid payload (#{reason}): #{invalid_document}"
22
+ exception = convert_reason_into_exceptions(exception_message, reason).first ||
23
+ Exceptions::InvalidDocument.new(exception_message)
24
+ raise exception
25
+ end
26
+ end
27
+
28
+ protected
29
+
30
+ def convert_reason_into_exceptions(exception_message, reason, values = [])
31
+ if reason.is_a?(String)
32
+ return convert_reason_message_into_error(exception_message, reason, values.join)
33
+ end
34
+ return nil unless reason.is_a?(Hash)
35
+ return nil unless reason.size == 1
36
+
37
+ reason.flat_map do |key, value|
38
+ convert_reason_into_exceptions(exception_message, value, (values + ["/#{key}"]))
39
+ end
40
+ end
41
+
42
+ def convert_reason_message_into_error(exception_message, reason, source)
43
+ payload = ExceptionPayload.new
44
+ payload.pointer = source.presence
45
+ payload.detail = reason
46
+ Exceptions::InvalidDocument.new(exception_message, payload)
47
+ end
48
+
49
+ def ams_deserializer_class
50
+ if defined?(ActiveModel::Serializer::Adapter::JsonApi::Deserialization)
51
+ ActiveModel::Serializer::Adapter::JsonApi::Deserialization
52
+ else
53
+ ActiveModelSerializers::Adapter::JsonApi::Deserialization
54
+ end
55
+ end
56
+
57
+ def ams_deserializer_config
58
+ @ams_deserializer_config ||= FunWithJsonAPi::DeserializerConfigBuilder.build(deserializer)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,11 @@
1
+ require 'fun_with_json_api/controller_methods'
2
+
3
+ module FunWithJsonApi
4
+ # Mountable engine for fun with json_api
5
+ class Railtie < Rails::Railtie
6
+ initializer 'fun_with_json_api.add_locales' do
7
+ translations = File.expand_path('../../../config/locales/**/*.{rb,yml}', __FILE__)
8
+ Dir.glob(translations) { |f| config.i18n.load_path << f.to_s }
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module FunWithJsonApi
2
+ VERSION = '0.0.1'.freeze
3
+ end
@@ -0,0 +1,24 @@
1
+ require 'fun_with_json_api/exception'
2
+ require 'fun_with_json_api/attribute'
3
+
4
+ require 'fun_with_json_api/pre_deserializer'
5
+ require 'fun_with_json_api/deserializer'
6
+ require 'fun_with_json_api/deserializer_config_builder'
7
+
8
+ # Makes working with JSON:API fun!
9
+ module FunWithJsonApi
10
+ module_function
11
+
12
+ def deserialize(api_document, deserializer_class, options = {})
13
+ # Prepare the deserializer and the expected config
14
+ deserializer = deserializer_class.create(options)
15
+
16
+ # Run through initial document structure validation and deserialization
17
+ unfiltered = FunWithJsonApi::PreDeserializer.parse(api_document, deserializer)
18
+
19
+ # Ensure document matches schema, and sanitize values
20
+ deserializer.sanitize_params(unfiltered)
21
+ end
22
+ end
23
+
24
+ require 'fun_with_json_api/railtie' if defined?(Rails)