fun_with_json_api 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 527661f615009228dc573ed949ccd8ea04349382
4
+ data.tar.gz: d93b9e012c63f7fab576f9c40c54af9bc3fd8c6e
5
+ SHA512:
6
+ metadata.gz: 59d7335c4ecb53d0b3b7800e6635ae52240d41750f626f443e567a7025b206245db1a362d13e6d5ee440b561c47e67b8acb7e0395ee511b9a039bf6f5177233f
7
+ data.tar.gz: 2daad3bbbb9ea0c93fc996bee1f0ded6dade569ef0223e60985a48b42c19aaa176ae4e4920146bb4855f0b344d19abe0508a9936ce06eccc4fa21106ef32455e
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2016 Ben Morrall
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'FunWithJsonApi'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ Bundler::GemHelper.install_tasks
18
+
19
+ require 'rubocop/rake_task'
20
+ RuboCop::RakeTask.new
21
+
22
+ require 'rspec/core'
23
+ require 'rspec/core/rake_task'
24
+
25
+ desc 'Run all specs in spec directory (excluding plugin specs)'
26
+ RSpec::Core::RakeTask.new(:spec)
27
+
28
+ task default: [:spec, :rubocop]
@@ -0,0 +1,13 @@
1
+ en:
2
+ fun_with_json_api:
3
+ exceptions:
4
+ invalid_document: 'Request json_api document is invalid'
5
+ invalid_attribute: 'Request json_api document attribute is invalid'
6
+ invalid_relationship: 'Request json_api document relationship is invalid'
7
+ missing_relationship: 'Unable to find the requested relationship'
8
+ invalid_boolean_attribute: "Boolean value should only be true, false, or null"
9
+ invalid_date_attribute: "Date value should be in the format YYYY-MM-DD"
10
+ invalid_datetime_attribute: "Datetime value should be a ISO 8601 datetime"
11
+ invalid_decimal_attribute: "Decimal value must be a decimal number (i.e. 123.45)"
12
+ invalid_float_attribute: "Float value must be a floating point number (i.e. 123.45)"
13
+ invalid_integer_attribute: "Integer value must be a integer number (i.e. 123)"
@@ -0,0 +1,38 @@
1
+ module FunWithJsonApi
2
+ class Attribute
3
+ attr_reader :name
4
+ attr_reader :as
5
+
6
+ def self.create(name, options = {})
7
+ format = options.fetch(:format, 'string')
8
+ attribute_class_name = "#{format.to_s.classify}Attribute"
9
+ if FunWithJsonApi::Attributes.const_defined?(attribute_class_name)
10
+ FunWithJsonApi::Attributes.const_get(attribute_class_name)
11
+ else
12
+ raise ArgumentError, "Unknown attribute type: #{format}"
13
+ end.new(name, options)
14
+ end
15
+
16
+ def initialize(name, options = {})
17
+ raise ArgumentError, 'name cannot be blank!' unless name.present?
18
+
19
+ @name = name
20
+ @as = options.fetch(:as, name)
21
+ end
22
+
23
+ def call(value)
24
+ value
25
+ end
26
+
27
+ def sanitize_attribute_method
28
+ :"parse_#{param_value}"
29
+ end
30
+
31
+ def param_value
32
+ as.to_sym
33
+ end
34
+ end
35
+ end
36
+
37
+ # Load pre-defined Attributes
38
+ Dir["#{File.dirname(__FILE__)}/attributes/**/*.rb"].each { |f| require f }
@@ -0,0 +1,24 @@
1
+ module FunWithJsonApi
2
+ module Attributes
3
+ # Ensures a value is either Boolean.TRUE, Boolean.FALSE or nil
4
+ # Raises an argument error otherwise
5
+ class BooleanAttribute < Attribute
6
+ def call(value)
7
+ return nil if value.nil?
8
+ return value if value.is_a?(TrueClass) || value.is_a?(FalseClass)
9
+
10
+ raise build_invalid_attribute_error(value)
11
+ end
12
+
13
+ private
14
+
15
+ def build_invalid_attribute_error(value)
16
+ exception_message = I18n.t('fun_with_json_api.exceptions.invalid_boolean_attribute')
17
+ payload = ExceptionPayload.new
18
+ payload.detail = exception_message
19
+ payload.pointer = "/data/attributes/#{name}"
20
+ Exceptions::InvalidAttribute.new(exception_message + ": #{value.inspect}", payload)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ module FunWithJsonApi
2
+ module Attributes
3
+ class DateAttribute < Attribute
4
+ DATE_FORMAT = '%Y-%m-%d'.freeze
5
+
6
+ def call(value)
7
+ Date.strptime(value, DATE_FORMAT) if value
8
+ rescue ArgumentError => exception
9
+ raise build_invalid_attribute_error(exception, value)
10
+ end
11
+
12
+ private
13
+
14
+ def build_invalid_attribute_error(exception, value)
15
+ payload = ExceptionPayload.new
16
+ payload.detail = I18n.t('fun_with_json_api.exceptions.invalid_date_attribute')
17
+ payload.pointer = "/data/attributes/#{name}"
18
+ Exceptions::InvalidAttribute.new(exception.message + ": #{value.inspect}", payload)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ module FunWithJsonApi
2
+ module Attributes
3
+ class DatetimeAttribute < Attribute
4
+ def call(value)
5
+ DateTime.iso8601(value) if value
6
+ rescue ArgumentError => exception
7
+ raise build_invalid_attribute_error(exception, value)
8
+ end
9
+
10
+ private
11
+
12
+ def build_invalid_attribute_error(exception, value)
13
+ payload = ExceptionPayload.new
14
+ payload.detail = I18n.t('fun_with_json_api.exceptions.invalid_datetime_attribute')
15
+ payload.pointer = "/data/attributes/#{name}"
16
+ Exceptions::InvalidAttribute.new(exception.message + ": #{value.inspect}", payload)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ module FunWithJsonApi
2
+ module Attributes
3
+ class DecimalAttribute < Attribute
4
+ def call(value)
5
+ if value
6
+ unless value.to_s =~ /[0-9]+(\.[0-9]+)?/
7
+ raise build_invalid_attribute_error(value)
8
+ end
9
+ BigDecimal.new(value.to_s)
10
+ end
11
+ end
12
+
13
+ protected
14
+
15
+ def build_invalid_attribute_error(value)
16
+ payload = ExceptionPayload.new
17
+ payload.detail = I18n.t('fun_with_json_api.exceptions.invalid_decimal_attribute')
18
+ payload.pointer = "/data/attributes/#{name}"
19
+ Exceptions::InvalidAttribute.new("Unable to parse decimal: #{value.inspect}", payload)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ module FunWithJsonApi
2
+ module Attributes
3
+ class FloatAttribute < FunWithJsonApi::Attribute
4
+ def call(value)
5
+ Float(value.to_s) if value
6
+ rescue ArgumentError => exception
7
+ raise build_invalid_attribute_error(exception, value)
8
+ end
9
+
10
+ private
11
+
12
+ def build_invalid_attribute_error(exception, value)
13
+ payload = ExceptionPayload.new
14
+ payload.detail = I18n.t('fun_with_json_api.exceptions.invalid_float_attribute')
15
+ payload.pointer = "/data/attributes/#{name}"
16
+ Exceptions::InvalidAttribute.new(exception.message + ": #{value.inspect}", payload)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module FunWithJsonApi
2
+ module Attributes
3
+ class IntegerAttribute < FunWithJsonApi::Attribute
4
+ def call(value)
5
+ Integer(value.to_s) if value
6
+ rescue ArgumentError => exception
7
+ raise build_invalid_attribute_error(exception)
8
+ end
9
+
10
+ private
11
+
12
+ def build_invalid_attribute_error(exception)
13
+ payload = ExceptionPayload.new
14
+ payload.detail = I18n.t('fun_with_json_api.exceptions.invalid_integer_attribute')
15
+ payload.pointer = "/data/attributes/#{name}"
16
+ Exceptions::InvalidAttribute.new(exception.message, payload)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,73 @@
1
+ module FunWithJsonApi
2
+ module Attributes
3
+ class Relationship < FunWithJsonApi::Attribute
4
+ # Creates a new Relationship with name
5
+ # @param name [String] name of the relationship
6
+ # @param deserializer_class_or_callable [Class] Class of Deserializer or
7
+ # a callable that returns one
8
+ # @param options[at] [String] alias value for the attribute
9
+ def self.create(name, deserializer_class_or_callable, options = {})
10
+ new(name, deserializer_class_or_callable, options)
11
+ end
12
+
13
+ delegate :id_param,
14
+ :type,
15
+ :resource_class,
16
+ to: :deserializer
17
+
18
+ def initialize(name, deserializer_class, options = {})
19
+ super(name, options)
20
+ @deserializer_class = deserializer_class
21
+ end
22
+
23
+ def deserializer
24
+ @deserializer ||= create_deserializer_from_deserializer_class
25
+ end
26
+
27
+ def call(id_value)
28
+ unless id_value.nil? || !id_value.is_a?(Array)
29
+ raise build_invalid_relationship_error(id_value)
30
+ end
31
+
32
+ resource_class.find_by!(id_param => id_value).try(:id) if id_value
33
+ rescue ActiveRecord::RecordNotFound => e
34
+ raise convert_record_not_found_error(e, id_value)
35
+ end
36
+
37
+ def param_value
38
+ :"#{as}_id"
39
+ end
40
+
41
+ private
42
+
43
+ # Creates a new Deserializer from the deserializer class
44
+ def create_deserializer_from_deserializer_class
45
+ if @deserializer_class.respond_to?(:call)
46
+ @deserializer_class.call
47
+ else
48
+ @deserializer_class
49
+ end.create(
50
+ attributes: [],
51
+ relationships: []
52
+ )
53
+ end
54
+
55
+ def build_invalid_relationship_error(id_value)
56
+ exception_message = "#{name} relationship should contain a single '#{type}' data hash"
57
+ payload = ExceptionPayload.new
58
+ payload.pointer = "/data/relationships/#{name}"
59
+ payload.detail = exception_message
60
+ Exceptions::InvalidRelationship.new(exception_message + ": #{id_value.inspect}", payload)
61
+ end
62
+
63
+ def convert_record_not_found_error(exception, id_value)
64
+ payload = ExceptionPayload.new
65
+ payload.pointer = "/data/relationships/#{name}/id"
66
+ payload.detail = "Unable to find '#{type}' with matching id: #{id_value.inspect}"
67
+ exception_message = "Couldn't find #{resource_class} where "\
68
+ "#{id_param} = #{id_value.inspect}: #{exception.message}"
69
+ Exceptions::MissingRelationship.new(exception_message, payload)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,99 @@
1
+ module FunWithJsonApi
2
+ module Attributes
3
+ class RelationshipCollection < FunWithJsonApi::Attribute
4
+ def self.create(name, deserializer_class_or_callable, options = {})
5
+ new(name, deserializer_class_or_callable, options)
6
+ end
7
+
8
+ delegate :id_param,
9
+ :type,
10
+ :resource_class,
11
+ to: :deserializer
12
+
13
+ def initialize(name, deserializer_class, options = {})
14
+ super(name, options.reverse_merge(as: name.to_s.singularize.to_sym))
15
+ @deserializer_class = deserializer_class
16
+
17
+ if as.to_s != as.to_s.singularize
18
+ raise ArgumentError, "Use a singular relationship as value: {as: :#{as.to_s.singularize}}"
19
+ end
20
+ end
21
+
22
+ def deserializer
23
+ @deserializer ||= create_deserializer_from_deserializer_class
24
+ end
25
+
26
+ # Expects an array of id values for a nested collection
27
+ def call(values)
28
+ unless values.nil? || values.is_a?(Array)
29
+ raise build_invalid_relationship_collection_error(values)
30
+ end
31
+
32
+ collection = resource_class.where(id_param => values)
33
+
34
+ # Ensure the collection size matches
35
+ expected_size = values.size
36
+ result_size = collection.size
37
+ if result_size != expected_size
38
+ raise build_missing_relationship_error_from_collection(collection, values)
39
+ end
40
+
41
+ # Call ActiceRecord#pluck if it is available
42
+ convert_collection_to_ids(collection)
43
+ end
44
+
45
+ # User the singular of `as` that is how AMS converts the value
46
+ def param_value
47
+ :"#{as}_ids"
48
+ end
49
+
50
+ private
51
+
52
+ def convert_collection_to_ids(collection)
53
+ if collection.respond_to? :pluck
54
+ # Well... pluck+arel doesn't work with SQLite, but select at least is safe
55
+ collection = collection.select(resource_class.arel_table[:id])
56
+ end
57
+ collection.map(&:id)
58
+ end
59
+
60
+ # Creates a new Deserializer from the deserializer class
61
+ def create_deserializer_from_deserializer_class
62
+ if @deserializer_class.respond_to?(:call)
63
+ @deserializer_class.call
64
+ else
65
+ @deserializer_class
66
+ end.create(
67
+ attributes: [],
68
+ relationships: []
69
+ )
70
+ end
71
+
72
+ def build_invalid_relationship_collection_error(values)
73
+ exception_message = "#{name} relationship should contain a array of '#{type}' data"
74
+ payload = ExceptionPayload.new
75
+ payload.pointer = "/data/relationships/#{name}"
76
+ payload.detail = exception_message
77
+ Exceptions::InvalidRelationship.new(exception_message + ": #{values.inspect}", payload)
78
+ end
79
+
80
+ def build_missing_relationship_error_from_collection(collection, values)
81
+ collection_values = collection.map { |resource| resource.public_send(id_param).to_s }
82
+ missing_values = values.reject { |value| collection_values.include?(value.to_s) }
83
+ payload = missing_values.map do |value|
84
+ build_missing_relationship_payload(value)
85
+ end
86
+ exception_message = "Couldn't find #{resource_class} items with "\
87
+ "#{id_param} in #{missing_values.inspect}"
88
+ Exceptions::MissingRelationship.new(exception_message, payload)
89
+ end
90
+
91
+ def build_missing_relationship_payload(value)
92
+ ExceptionPayload.new.tap do |payload|
93
+ payload.pointer = "/data/relationships/#{name}/id"
94
+ payload.detail = "Unable to find '#{type}' with matching id: #{value.inspect}"
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,9 @@
1
+ module FunWithJsonApi
2
+ module Attributes
3
+ class StringAttribute < Attribute
4
+ def call(value)
5
+ value.to_s if value
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ require 'fun_with_json_api/exception_serializer'
2
+
3
+ module FunWithJsonApi
4
+ module ControllerMethods
5
+ def render_fun_with_json_api_exception(exception)
6
+ render json: exception,
7
+ serializer: FunWithJsonApi::ExceptionSerializer,
8
+ adapter: :json,
9
+ status: exception.http_status
10
+ end
11
+ end
12
+ end