fun_with_json_api 0.0.2 → 0.0.3

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +4 -1
  3. data/config/locales/fun_with_json_api.en.yml +29 -2
  4. data/lib/fun_with_json_api.rb +30 -2
  5. data/lib/fun_with_json_api/action_controller_extensions/serialization.rb +18 -0
  6. data/lib/fun_with_json_api/attribute.rb +3 -3
  7. data/lib/fun_with_json_api/attributes/relationship.rb +37 -23
  8. data/lib/fun_with_json_api/attributes/relationship_collection.rb +55 -38
  9. data/lib/fun_with_json_api/attributes/string_attribute.rb +12 -1
  10. data/lib/fun_with_json_api/attributes/uuid_v4_attribute.rb +27 -0
  11. data/lib/fun_with_json_api/controller_methods.rb +1 -1
  12. data/lib/fun_with_json_api/deserializer.rb +61 -8
  13. data/lib/fun_with_json_api/deserializer_class_methods.rb +37 -7
  14. data/lib/fun_with_json_api/exceptions/illegal_client_generated_identifier.rb +17 -0
  15. data/lib/fun_with_json_api/exceptions/invalid_client_generated_identifier.rb +17 -0
  16. data/lib/fun_with_json_api/exceptions/invalid_document_identifier.rb +17 -0
  17. data/lib/fun_with_json_api/exceptions/invalid_document_type.rb +20 -0
  18. data/lib/fun_with_json_api/exceptions/invalid_relationship.rb +5 -3
  19. data/lib/fun_with_json_api/exceptions/invalid_relationship_type.rb +17 -0
  20. data/lib/fun_with_json_api/exceptions/missing_resource.rb +15 -0
  21. data/lib/fun_with_json_api/exceptions/unknown_attribute.rb +15 -0
  22. data/lib/fun_with_json_api/exceptions/unknown_relationship.rb +15 -0
  23. data/lib/fun_with_json_api/find_collection_from_document.rb +124 -0
  24. data/lib/fun_with_json_api/find_resource_from_document.rb +112 -0
  25. data/lib/fun_with_json_api/pre_deserializer.rb +1 -0
  26. data/lib/fun_with_json_api/railtie.rb +30 -1
  27. data/lib/fun_with_json_api/schema_validator.rb +47 -0
  28. data/lib/fun_with_json_api/schema_validators/check_attributes.rb +52 -0
  29. data/lib/fun_with_json_api/schema_validators/check_document_id_matches_resource.rb +96 -0
  30. data/lib/fun_with_json_api/schema_validators/check_document_type_matches_resource.rb +40 -0
  31. data/lib/fun_with_json_api/schema_validators/check_relationships.rb +127 -0
  32. data/lib/fun_with_json_api/version.rb +1 -1
  33. data/spec/dummy/log/test.log +172695 -0
  34. data/spec/fixtures/active_record.rb +6 -0
  35. data/spec/fun_with_json_api/controller_methods_spec.rb +8 -3
  36. data/spec/fun_with_json_api/deserializer_class_methods_spec.rb +14 -6
  37. data/spec/fun_with_json_api/deserializer_spec.rb +155 -40
  38. data/spec/fun_with_json_api/exception_spec.rb +9 -9
  39. data/spec/fun_with_json_api/find_collection_from_document_spec.rb +203 -0
  40. data/spec/fun_with_json_api/find_resource_from_document_spec.rb +100 -0
  41. data/spec/fun_with_json_api/pre_deserializer_spec.rb +26 -26
  42. data/spec/fun_with_json_api/railtie_spec.rb +88 -0
  43. data/spec/fun_with_json_api/schema_validator_spec.rb +94 -0
  44. data/spec/fun_with_json_api/schema_validators/check_attributes_spec.rb +52 -0
  45. data/spec/fun_with_json_api/schema_validators/check_document_id_matches_resource_spec.rb +115 -0
  46. data/spec/fun_with_json_api/schema_validators/check_document_type_matches_resource_spec.rb +30 -0
  47. data/spec/fun_with_json_api/schema_validators/check_relationships_spec.rb +150 -0
  48. data/spec/fun_with_json_api_spec.rb +148 -4
  49. metadata +49 -4
  50. data/spec/example_spec.rb +0 -64
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b39d90754d8cde854f6cd239634dd7415ff190b1
4
- data.tar.gz: c7b727d00f76668701739f0e3a7f6abaf1438fce
3
+ metadata.gz: cbb6fa5d08ff453c7adef23b0c5a608e98897228
4
+ data.tar.gz: 1cb7de4f0f03fc553af3da253fea06bf33704cbe
5
5
  SHA512:
6
- metadata.gz: b70869284cacc37b7915f8dc17d819184c722620e6350098f8ddfb22934cf15e6358c12ba455da2f3214f9c5969d7b1dfda0914eced2c9e0930675984d4284fe
7
- data.tar.gz: 4af352dff332ab0770a74eed228c53e081da20f32925532c4546b774a61c41ba360e0247cb3fc4a8550c97822e7daebc2552247c5274a487d89c96b83d7e63ad
6
+ metadata.gz: be8824b7d81fc131fe50591cb9ff83495407d17f053845279add97a6eeccd8a473dba036df315f45db3b7097d84c9105fc6d1228308d195edc488dbfde054a9f
7
+ data.tar.gz: 2d5c7af796c4224c4868a66bf876d447337304ac9616da5a254f0253e4563dd003700774187f2075ef76e6d28b12551974c00c20fb203d559fa15a23929506fd
data/Rakefile CHANGED
@@ -17,7 +17,10 @@ end
17
17
  Bundler::GemHelper.install_tasks
18
18
 
19
19
  require 'rubocop/rake_task'
20
- RuboCop::RakeTask.new
20
+ require 'rubocop-rspec'
21
+ RuboCop::RakeTask.new do |task|
22
+ task.requires << 'rubocop-rspec'
23
+ end
21
24
 
22
25
  require 'rspec/core'
23
26
  require 'rspec/core/rake_task'
@@ -2,12 +2,39 @@ en:
2
2
  fun_with_json_api:
3
3
  exceptions:
4
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'
5
+ invalid_document_identifier: 'Request json_api data id is invalid'
6
+ invalid_client_generated_identifier: 'Request json_api data id has already been used for an existing resource'
7
+ illegal_client_generated_identifier: 'Request json_api attempted to set an unsupported client-generated id'
8
+ invalid_document_type: 'Request json_api data type does not match endpoint'
9
+ missing_resource: 'Unable to find the requested resource'
10
+ invalid_attribute: 'Request json_api attribute data is invalid'
11
+ unknown_attribute: 'Request json_api attribute is unsupported by the current endpoint'
12
+ invalid_relationship: 'Request json_api relationship data is invalid'
7
13
  missing_relationship: 'Unable to find the requested relationship'
14
+ unknown_relationship: 'Request json_api relationship is unsupported by the current endpoint'
15
+ invalid_relationship_type: 'Request json_api relationship type does not match expected resource'
8
16
  invalid_boolean_attribute: "Boolean value should only be true, false, or null"
9
17
  invalid_date_attribute: "Date value should be in the format YYYY-MM-DD"
10
18
  invalid_datetime_attribute: "Datetime value should be a ISO 8601 datetime"
11
19
  invalid_decimal_attribute: "Decimal value must be a decimal number (i.e. 123.45)"
12
20
  invalid_float_attribute: "Float value must be a floating point number (i.e. 123.45)"
13
21
  invalid_integer_attribute: "Integer value must be a integer number (i.e. 123)"
22
+ invalid_string_attribute: 'String value must be a JSON String (i.e. "Example")'
23
+ invalid_uuid_v4_attribute: 'UUID value must be RFC 4122 Version 4 UUID (i.e. "f47ac10b-58cc-4372-a567-0e02b2c3d479")'
24
+ schema_validators:
25
+ document_id_does_not_match_resource: "Expected data id to match resource at endpoint: %{expected}"
26
+ document_type_does_not_match_endpoint: "Expected data type to be a '%{expected}' resource"
27
+ invalid_relationship_type_in_array: "Expected '%{relationship}' relationship to be an Array of '%{relationship_type}' resource identifiers"
28
+ invalid_relationship_type_in_hash: "Expected '%{relationship}' relationship to be null or a '%{relationship_type}' resource identifier Hash"
29
+ resource_id_can_not_be_client_generated: "The current endpoint does not allow you to set an id for a new '%{resource}' resource"
30
+ resource_id_has_already_been_assigned: "The provided id for a new '%{resource}' resource has already been used by another resource: %{id}"
31
+ unknown_attribute_for_resource: "The provided attribute '%{attribute}' can not be assigned to a '%{resource}' resource from the current endpoint"
32
+ unknown_relationship_for_resource: "The provided relationship '%{relationship}' can not be assigned to a '%{resource}' resource from the current endpoint"
33
+ find_resource_from_document:
34
+ invalid_document: "Expected data to be a Hash or null"
35
+ invalid_document_type: "Expected data type to be a '%{resource}' resource"
36
+ missing_resource: "Unable to find '%{resource}' with matching id: '%{resource_id}'"
37
+ find_collection_from_document:
38
+ invalid_document: "Expected data to be a Array of '%{resource}' resources"
39
+ invalid_document_type: "Expected '%{type}' to be a '%{resource}' resource"
40
+ missing_resource: "Unable to find '%{resource}' with matching id: '%{resource_id}'"
@@ -3,22 +3,50 @@ require 'fun_with_json_api/attribute'
3
3
 
4
4
  require 'fun_with_json_api/pre_deserializer'
5
5
  require 'fun_with_json_api/deserializer'
6
- require 'fun_with_json_api/deserializer_config_builder'
6
+ require 'fun_with_json_api/schema_validator'
7
+ require 'fun_with_json_api/find_collection_from_document'
8
+ require 'fun_with_json_api/find_resource_from_document'
7
9
 
8
10
  # Makes working with JSON:API fun!
9
11
  module FunWithJsonApi
12
+ MEDIA_TYPE = 'application/vnd.api+json'.freeze
13
+
10
14
  module_function
11
15
 
12
- def deserialize(api_document, deserializer_class, options = {})
16
+ def deserialize(api_document, deserializer_class, resource = nil, options = {})
13
17
  # Prepare the deserializer and the expected config
14
18
  deserializer = deserializer_class.create(options)
15
19
 
16
20
  # Run through initial document structure validation and deserialization
17
21
  unfiltered = FunWithJsonApi::PreDeserializer.parse(api_document, deserializer)
18
22
 
23
+ # Check the document matches up with expected resource parameters
24
+ FunWithJsonApi::SchemaValidator.check(api_document, deserializer, resource)
25
+
19
26
  # Ensure document matches schema, and sanitize values
20
27
  deserializer.sanitize_params(unfiltered)
21
28
  end
29
+
30
+ def deserialize_resource(api_document, deserializer_class, resource, options = {})
31
+ raise ArgumentError, 'resource cannot be nil' if resource.nil?
32
+ deserialize(api_document, deserializer_class, resource, options)
33
+ end
34
+
35
+ def find_resource(api_document, deserializer_class, options = {})
36
+ # Prepare the deserializer for loading a resource
37
+ deserializer = deserializer_class.create(options.merge(attributes: [], relationships: []))
38
+
39
+ # Load the resource from the document id
40
+ FunWithJsonApi::FindResourceFromDocument.find(api_document, deserializer)
41
+ end
42
+
43
+ def find_collection(api_document, deserializer_class, options = {})
44
+ # Prepare the deserializer for loading a resource
45
+ deserializer = deserializer_class.create(options.merge(attributes: [], relationships: []))
46
+
47
+ # Load the collection from the document
48
+ FunWithJsonApi::FindCollectionFromDocument.find(api_document, deserializer)
49
+ end
22
50
  end
23
51
 
24
52
  require 'fun_with_json_api/railtie' if defined?(Rails)
@@ -0,0 +1,18 @@
1
+ module FunWithJsonApi
2
+ module ActionControllerExtensions
3
+ module Serialization
4
+ # Overrides the dynamic render json_api methods to use ActiveModelSerializer
5
+ [:_render_option_json_api, :_render_with_renderer_json_api].each do |renderer_method|
6
+ define_method renderer_method do |resource, options|
7
+ options.fetch(:adapter) { options[:adapter] ||= :json_api }
8
+ options.fetch(:serialization_context) do
9
+ options[:serialization_context] ||=
10
+ ActiveModelSerializers::SerializationContext.new(request)
11
+ end
12
+ serializable_resource = get_serializer(resource, options)
13
+ super(serializable_resource, options)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -16,8 +16,8 @@ module FunWithJsonApi
16
16
  def initialize(name, options = {})
17
17
  raise ArgumentError, 'name cannot be blank!' unless name.present?
18
18
 
19
- @name = name
20
- @as = options.fetch(:as, name)
19
+ @name = name.to_sym
20
+ @as = options.fetch(:as, name).to_sym
21
21
  end
22
22
 
23
23
  def call(value)
@@ -29,7 +29,7 @@ module FunWithJsonApi
29
29
  end
30
30
 
31
31
  def param_value
32
- as.to_sym
32
+ as
33
33
  end
34
34
  end
35
35
  end
@@ -10,18 +10,17 @@ module FunWithJsonApi
10
10
  new(name, deserializer_class_or_callable, options)
11
11
  end
12
12
 
13
- delegate :id_param,
14
- :type,
15
- :resource_class,
16
- to: :deserializer
13
+ attr_reader :deserializer_class
14
+ attr_reader :options
15
+ delegate :type, to: :deserializer
17
16
 
18
17
  def initialize(name, deserializer_class, options = {})
19
18
  super(name, options)
20
19
  @deserializer_class = deserializer_class
21
- end
22
-
23
- def deserializer
24
- @deserializer ||= create_deserializer_from_deserializer_class
20
+ @options = options.reverse_merge(
21
+ attributes: [],
22
+ relationships: []
23
+ )
25
24
  end
26
25
 
27
26
  def call(id_value)
@@ -29,44 +28,59 @@ module FunWithJsonApi
29
28
  raise build_invalid_relationship_error(id_value)
30
29
  end
31
30
 
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)
31
+ resource = deserializer.load_resource_from_id_value(id_value)
32
+ return resource.id if resource
33
+
34
+ raise build_missing_relationship_error(id_value)
35
+ end
36
+
37
+ # rubocop:disable Style/PredicateName
38
+
39
+ def has_many?
40
+ false
35
41
  end
36
42
 
43
+ # rubocop:enable Style/PredicateName
44
+
37
45
  def param_value
38
46
  :"#{as}_id"
39
47
  end
40
48
 
49
+ def deserializer
50
+ @deserializer ||= build_deserializer_from_options
51
+ end
52
+
41
53
  private
42
54
 
43
- # Creates a new Deserializer from the deserializer class
44
- def create_deserializer_from_deserializer_class
55
+ def build_deserializer_from_options
45
56
  if @deserializer_class.respond_to?(:call)
46
57
  @deserializer_class.call
47
58
  else
48
59
  @deserializer_class
49
- end.create(
50
- attributes: [],
51
- relationships: []
52
- )
60
+ end.create(options)
53
61
  end
54
62
 
55
63
  def build_invalid_relationship_error(id_value)
56
- exception_message = "#{name} relationship should contain a single '#{type}' data hash"
64
+ exception_message = "#{name} relationship should contain a single '#{deserializer.type}'"\
65
+ ' data hash'
57
66
  payload = ExceptionPayload.new
58
67
  payload.pointer = "/data/relationships/#{name}"
59
68
  payload.detail = exception_message
60
69
  Exceptions::InvalidRelationship.new(exception_message + ": #{id_value.inspect}", payload)
61
70
  end
62
71
 
63
- def convert_record_not_found_error(exception, id_value)
72
+ def build_missing_relationship_error(id_value, message = nil)
73
+ message ||= missing_resource_debug_message(id_value)
64
74
  payload = ExceptionPayload.new
65
75
  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)
76
+ payload.detail = "Unable to find '#{deserializer.type}' with matching id"\
77
+ ": #{id_value.inspect}"
78
+ Exceptions::MissingRelationship.new(message, payload)
79
+ end
80
+
81
+ def missing_resource_debug_message(id_value)
82
+ "Couldn't find #{deserializer.resource_class.name}"\
83
+ " where #{deserializer.id_param} = #{id_value.inspect}"
70
84
  end
71
85
  end
72
86
  end
@@ -5,22 +5,19 @@ module FunWithJsonApi
5
5
  new(name, deserializer_class_or_callable, options)
6
6
  end
7
7
 
8
- delegate :id_param,
9
- :type,
10
- :resource_class,
11
- to: :deserializer
8
+ attr_reader :deserializer_class
9
+ attr_reader :options
10
+ delegate :type, to: :deserializer
12
11
 
13
12
  def initialize(name, deserializer_class, options = {})
14
13
  super(name, options.reverse_merge(as: name.to_s.singularize.to_sym))
15
14
  @deserializer_class = deserializer_class
15
+ @options = options.reverse_merge(
16
+ attributes: [],
17
+ relationships: []
18
+ )
16
19
 
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
20
+ check_as_attribute_is_singular!
24
21
  end
25
22
 
26
23
  # Expects an array of id values for a nested collection
@@ -29,7 +26,7 @@ module FunWithJsonApi
29
26
  raise build_invalid_relationship_collection_error(values)
30
27
  end
31
28
 
32
- collection = resource_class.where(id_param => values)
29
+ collection = deserializer.load_collection_from_id_values(values)
33
30
 
34
31
  # Ensure the collection size matches
35
32
  expected_size = values.size
@@ -42,35 +39,50 @@ module FunWithJsonApi
42
39
  convert_collection_to_ids(collection)
43
40
  end
44
41
 
42
+ # rubocop:disable Style/PredicateName
43
+
44
+ def has_many?
45
+ true
46
+ end
47
+
48
+ # rubocop:enable Style/PredicateName
49
+
45
50
  # User the singular of `as` that is how AMS converts the value
46
51
  def param_value
47
52
  :"#{as}_ids"
48
53
  end
49
54
 
55
+ def deserializer
56
+ @deserializer ||= build_deserializer_from_options
57
+ end
58
+
50
59
  private
51
60
 
61
+ def build_deserializer_from_options
62
+ if @deserializer_class.respond_to?(:call)
63
+ @deserializer_class.call
64
+ else
65
+ @deserializer_class
66
+ end.create(options)
67
+ end
68
+
69
+ def check_as_attribute_is_singular!
70
+ if as.to_s != as.to_s.singularize
71
+ raise ArgumentError, "Use a singular relationship as value: {as: :#{as.to_s.singularize}}"
72
+ end
73
+ end
74
+
52
75
  def convert_collection_to_ids(collection)
53
76
  if collection.respond_to? :pluck
54
77
  # Well... pluck+arel doesn't work with SQLite, but select at least is safe
55
- collection = collection.select(resource_class.arel_table[:id])
78
+ collection = collection.select(deserializer.resource_class.arel_table[:id])
56
79
  end
57
80
  collection.map(&:id)
58
81
  end
59
82
 
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
83
  def build_invalid_relationship_collection_error(values)
73
- exception_message = "#{name} relationship should contain a array of '#{type}' data"
84
+ exception_message = "#{name} relationship should contain a array of"\
85
+ " '#{deserializer.type}' data"
74
86
  payload = ExceptionPayload.new
75
87
  payload.pointer = "/data/relationships/#{name}"
76
88
  payload.detail = exception_message
@@ -78,21 +90,26 @@ module FunWithJsonApi
78
90
  end
79
91
 
80
92
  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}"
93
+ collection_ids = deserializer.format_collection_ids(collection)
94
+
95
+ payload = build_missing_relationship_payload(collection_ids, values)
96
+
97
+ missing_values = values.reject { |value| collection_ids.include?(value.to_s) }
98
+ exception_message = "Couldn't find #{deserializer.resource_class} items with "\
99
+ "#{deserializer.id_param} in #{missing_values.inspect}"
88
100
  Exceptions::MissingRelationship.new(exception_message, payload)
89
101
  end
90
102
 
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
103
+ def build_missing_relationship_payload(collection_ids, values)
104
+ values.each_with_index.map do |resource_id, index|
105
+ next if collection_ids.include?(resource_id)
106
+
107
+ ExceptionPayload.new.tap do |payload|
108
+ payload.pointer = "/data/relationships/#{name}/#{index}/id"
109
+ payload.detail = "Unable to find '#{deserializer.type}' with matching id"\
110
+ ": \"#{resource_id}\""
111
+ end
112
+ end.reject(&:nil?)
96
113
  end
97
114
  end
98
115
  end
@@ -2,7 +2,18 @@ module FunWithJsonApi
2
2
  module Attributes
3
3
  class StringAttribute < Attribute
4
4
  def call(value)
5
- value.to_s if value
5
+ return value if value.nil? || value.is_a?(String)
6
+
7
+ raise build_invalid_attribute_error(value)
8
+ end
9
+
10
+ private
11
+
12
+ def build_invalid_attribute_error(value)
13
+ payload = ExceptionPayload.new
14
+ payload.detail = I18n.t('fun_with_json_api.exceptions.invalid_string_attribute')
15
+ payload.pointer = "/data/attributes/#{name}"
16
+ Exceptions::InvalidAttribute.new("Value is not a string: #{value.class.name}", payload)
6
17
  end
7
18
  end
8
19
  end
@@ -0,0 +1,27 @@
1
+ module FunWithJsonApi
2
+ module Attributes
3
+ # Attribute that only accepts a properly generated and formatted UUID version 4
4
+ # as described in RFC 4122
5
+ class UuidV4Attribute < Attribute
6
+ # http://blog.arkency.com/2014/10/how-to-start-using-uuid-in-activerecord-with-postgresql/
7
+ UUID_V4_REGEX = /\A[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}\z/
8
+
9
+ def call(value)
10
+ return value if value.nil? || value =~ UUID_V4_REGEX
11
+
12
+ raise build_invalid_attribute_error(value)
13
+ end
14
+
15
+ private
16
+
17
+ def build_invalid_attribute_error(value)
18
+ payload = ExceptionPayload.new
19
+ payload.detail = I18n.t('fun_with_json_api.exceptions.invalid_uuid_v4_attribute')
20
+ payload.pointer = "/data/attributes/#{name}"
21
+ Exceptions::InvalidAttribute.new(
22
+ "Value is not a RFC 4122 Version 4 UUID: #{value.class.name}", payload
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end