smooth_operator 0.4.4 → 1.2.0

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 (68) hide show
  1. checksums.yaml +9 -9
  2. data/.gitignore +2 -1
  3. data/.rspec +4 -0
  4. data/Gemfile +13 -0
  5. data/README.md +258 -10
  6. data/console.rb +44 -0
  7. data/lib/smooth_operator/array_with_meta_data.rb +31 -0
  8. data/lib/smooth_operator/attribute_assignment.rb +102 -0
  9. data/lib/smooth_operator/attribute_methods.rb +87 -0
  10. data/lib/smooth_operator/attributes/base.rb +107 -0
  11. data/lib/smooth_operator/attributes/dirty.rb +29 -0
  12. data/lib/smooth_operator/attributes/normal.rb +15 -0
  13. data/lib/smooth_operator/delegation.rb +60 -0
  14. data/lib/smooth_operator/finder_methods.rb +43 -0
  15. data/lib/smooth_operator/helpers.rb +79 -0
  16. data/lib/smooth_operator/model_schema.rb +81 -0
  17. data/lib/smooth_operator/open_struct.rb +37 -0
  18. data/lib/smooth_operator/operator.rb +145 -0
  19. data/lib/smooth_operator/operators/faraday.rb +75 -0
  20. data/lib/smooth_operator/operators/typhoeus.rb +77 -0
  21. data/lib/smooth_operator/persistence.rb +144 -0
  22. data/lib/smooth_operator/relation/array_relation.rb +13 -0
  23. data/lib/smooth_operator/relation/association_reflection.rb +75 -0
  24. data/lib/smooth_operator/relation/associations.rb +75 -0
  25. data/lib/smooth_operator/relation/reflection.rb +41 -0
  26. data/lib/smooth_operator/relation/single_relation.rb +14 -0
  27. data/lib/smooth_operator/remote_call/base.rb +80 -0
  28. data/lib/smooth_operator/remote_call/errors/connection_failed.rb +20 -0
  29. data/lib/smooth_operator/remote_call/errors/timeout.rb +20 -0
  30. data/lib/smooth_operator/remote_call/faraday.rb +19 -0
  31. data/lib/smooth_operator/remote_call/typhoeus.rb +19 -0
  32. data/lib/smooth_operator/serialization.rb +79 -0
  33. data/lib/smooth_operator/translation.rb +27 -0
  34. data/lib/smooth_operator/validations.rb +15 -0
  35. data/lib/smooth_operator/version.rb +1 -1
  36. data/lib/smooth_operator.rb +26 -5
  37. data/smooth_operator.gemspec +12 -3
  38. data/spec/factories/user_factory.rb +34 -0
  39. data/spec/require_helper.rb +11 -0
  40. data/spec/smooth_operator/attribute_assignment_spec.rb +351 -0
  41. data/spec/smooth_operator/attributes_dirty_spec.rb +53 -0
  42. data/spec/smooth_operator/delegation_spec.rb +139 -0
  43. data/spec/smooth_operator/finder_methods_spec.rb +105 -0
  44. data/spec/smooth_operator/model_schema_spec.rb +31 -0
  45. data/spec/smooth_operator/operator_spec.rb +46 -0
  46. data/spec/smooth_operator/persistence_spec.rb +424 -0
  47. data/spec/smooth_operator/remote_call_spec.rb +320 -0
  48. data/spec/smooth_operator/serialization_spec.rb +80 -0
  49. data/spec/smooth_operator/validations_spec.rb +42 -0
  50. data/spec/spec_helper.rb +25 -0
  51. data/spec/support/helpers/persistence_helper.rb +38 -0
  52. data/spec/support/localhost_server.rb +97 -0
  53. data/spec/support/models/address.rb +14 -0
  54. data/spec/support/models/comment.rb +3 -0
  55. data/spec/support/models/post.rb +13 -0
  56. data/spec/support/models/user.rb +41 -0
  57. data/spec/support/models/user_with_address_and_posts.rb +89 -0
  58. data/spec/support/test_server.rb +165 -0
  59. metadata +108 -18
  60. data/lib/smooth_operator/base.rb +0 -30
  61. data/lib/smooth_operator/core.rb +0 -218
  62. data/lib/smooth_operator/http_handlers/typhoeus/base.rb +0 -58
  63. data/lib/smooth_operator/http_handlers/typhoeus/orm.rb +0 -34
  64. data/lib/smooth_operator/http_handlers/typhoeus/remote_call.rb +0 -28
  65. data/lib/smooth_operator/operator/base.rb +0 -43
  66. data/lib/smooth_operator/operator/exceptions.rb +0 -64
  67. data/lib/smooth_operator/operator/orm.rb +0 -118
  68. data/lib/smooth_operator/operator/remote_call.rb +0 -84
@@ -0,0 +1,107 @@
1
+ module SmoothOperator
2
+ module Attributes
3
+
4
+ class Base
5
+
6
+ protected ##################### PROTECTED ########################
7
+
8
+ def cast_to_type(name, value, parent_object)
9
+ known_by_schema, type, unknown_hash_class = parent_object.known_by_schema?(name), parent_object.get_attribute_type(name), parent_object.class.unknown_hash_class
10
+
11
+ return Helpers.duplicate(value) if known_by_schema && type.nil?
12
+
13
+ case value
14
+ when Array
15
+ value.map { |array_entry| self.class.new(name, array_entry, parent_object).value }
16
+ when Hash
17
+ type.nil? ? new_unknown_hash(value, unknown_hash_class, parent_object) : type.new(value, parent_object: parent_object)
18
+ else
19
+ convert(value, type)
20
+ end
21
+ end
22
+
23
+ def convert(value, type)
24
+ case type
25
+
26
+ when :string, :text, String
27
+ value.to_s
28
+
29
+ when :int, :integer, Integer, Fixnum
30
+ to_int(value)
31
+
32
+ when :date, Date
33
+ to_date(value)
34
+
35
+ when :float, Float
36
+ to_float(value)
37
+
38
+ when :bool, :boolean
39
+ to_boolean(value)
40
+
41
+ when :datetime, :date_time, DateTime
42
+ to_datetime(value)
43
+
44
+ else
45
+ Helpers.duplicate(value)
46
+ end
47
+ end
48
+
49
+ def to_date(string)
50
+ return string if string.is_a?(Date)
51
+
52
+ Date.parse(string) rescue nil
53
+ end
54
+
55
+ def to_datetime(string)
56
+ return string if string.is_a?(DateTime)
57
+
58
+ DateTime.parse(string) rescue nil
59
+ end
60
+
61
+ def to_boolean(string)
62
+ value = string.to_s.downcase
63
+
64
+ ['1', 'true'].include?(value) ? true : ['0', 'false'].include?(value) ? false : nil
65
+ end
66
+
67
+ def to_int(string)
68
+ return string if string.is_a?(Fixnum)
69
+
70
+ to_float(string).to_i
71
+ end
72
+
73
+ def to_float(string)
74
+ return string if string.is_a?(Float)
75
+
76
+ return 0 if string.nil? || !(string.is_a?(String) || string.is_a?(Fixnum))
77
+
78
+ value = string.to_s.gsub(',', '.').scan(/-*\d+[.]*\d*/).flatten.map(&:to_f).first
79
+
80
+ value.nil? ? 0 : value
81
+ end
82
+
83
+ def new_unknown_hash(hash, unknown_hash_class, parent_object)
84
+ if unknown_hash_class == :none
85
+ hash
86
+ else
87
+ unknown_hash_class.new(cast_params(hash, unknown_hash_class, parent_object))
88
+ end
89
+ end
90
+
91
+
92
+ private ################### PRIVATE #####################
93
+
94
+ def cast_params(attributes, unknown_hash_class, parent_object)
95
+ hash = {}
96
+
97
+ attributes.each do |key, value|
98
+ hash[key] = cast_to_type(key, value, parent_object)
99
+ end
100
+
101
+ hash
102
+ end
103
+
104
+ end
105
+
106
+ end
107
+ end
@@ -0,0 +1,29 @@
1
+ module SmoothOperator
2
+ module Attributes
3
+
4
+ class Dirty < Base
5
+
6
+ attr_reader :original_name, :original_value, :first_value, :value
7
+
8
+ def initialize(name, value, parent_object)
9
+ @original_name, @original_value = name, value
10
+
11
+ @first_value = set_value(value, parent_object)
12
+ end
13
+
14
+ def set_value(new_value, parent_object)
15
+ @value = cast_to_type(original_name, new_value, parent_object)
16
+ end
17
+
18
+ def changed?
19
+ @first_value != @value
20
+ end
21
+
22
+ def was
23
+ @first_value
24
+ end
25
+
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,15 @@
1
+ module SmoothOperator
2
+ module Attributes
3
+
4
+ class Normal < Base
5
+
6
+ attr_reader :value
7
+
8
+ def initialize(name, value, parent_object)
9
+ @value = cast_to_type(name, value, parent_object)
10
+ end
11
+
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,60 @@
1
+ module SmoothOperator
2
+
3
+ module Delegation
4
+
5
+ def respond_to?(method)
6
+ if known_attribute?(method)
7
+ true
8
+ else
9
+ self.class.reflect_on_association(method) ? true : super
10
+ end
11
+ end
12
+
13
+ def method_missing(method, *args, &block)
14
+ method_type, method_name = *parse_method(method)
15
+
16
+ result = case method_type
17
+ when :was
18
+ get_internal_data(method_name, :was)
19
+ when :changed
20
+ get_internal_data(method_name, :changed?)
21
+ when :setter
22
+ return push_to_internal_data(method_name, args.first)
23
+ else
24
+ if Helpers.safe_call(self.class, :reflect_on_association, method)
25
+ return get_relation(method_name)
26
+ elsif !self.class.strict_behaviour || known_attribute?(method_name)
27
+ return get_internal_data(method_name)
28
+ end
29
+ end
30
+
31
+ result.nil? ? super : result
32
+ end
33
+
34
+
35
+ protected #################### PROTECTED ################
36
+
37
+ def parse_method(method)
38
+ method = method.to_s
39
+
40
+ if method?(method, /=$/)
41
+ [:setter, method[0..-2]]
42
+ elsif method?(method, /_was$/)
43
+ [:was, method[0..-5]]
44
+ elsif method?(method, /_changed\?$/)
45
+ [:changed, method[0..-10]]
46
+ else
47
+ [nil, method]
48
+ end
49
+ end
50
+
51
+
52
+ private #################### PRIVATE ################
53
+
54
+ def method?(method, regex)
55
+ !! ((method.to_s) =~ regex)
56
+ end
57
+
58
+ end
59
+
60
+ end
@@ -0,0 +1,43 @@
1
+ require 'smooth_operator/array_with_meta_data'
2
+
3
+ module SmoothOperator
4
+
5
+ module FinderMethods
6
+
7
+ def find(relative_path, data = {}, options = {})
8
+ relative_path = '' if relative_path == :all
9
+
10
+ get(relative_path, data, options) do |remote_call|
11
+ remote_call.object = build_object(remote_call.parsed_response, options) if remote_call.ok?
12
+
13
+ block_given? ? yield(remote_call) : remote_call
14
+ end
15
+ end
16
+
17
+
18
+ protected #################### PROTECTED ##################
19
+
20
+ def build_object(parsed_response, options, from_array = false)
21
+ if parsed_response.is_a?(Array)
22
+ parsed_response.map { |array_entry| build_object(array_entry, options, true) }
23
+ elsif parsed_response.is_a?(Hash)
24
+ if parsed_response.include?(object_class.resources_name) && !from_array
25
+ ArrayWithMetaData.new(parsed_response, object_class)
26
+ else
27
+ object_class.new(parsed_response, from_server: true)
28
+ end
29
+ else
30
+ parsed_response
31
+ end
32
+ end
33
+
34
+
35
+ private #################### PRIVATE ##################
36
+
37
+ def object_class
38
+ @object_class ||= self.class == Class ? self : self.class
39
+ end
40
+
41
+ end
42
+
43
+ end
@@ -0,0 +1,79 @@
1
+ module SmoothOperator
2
+
3
+ module Helpers
4
+
5
+ extend self
6
+
7
+ def safe_call(object, method, *args)
8
+ if object.respond_to?(method)
9
+ object.send(method, *args)
10
+ else
11
+ false
12
+ end
13
+ end
14
+
15
+ def super_method(object, method_name, *args)
16
+ object.superclass.send(method_name, *args) if object.superclass.respond_to?(method_name)
17
+ end
18
+
19
+ def get_instance_variable(object, variable, default_value)
20
+ instance_var = object.instance_variable_get("@#{variable}")
21
+
22
+ return instance_var unless instance_var.nil?
23
+
24
+ instance_var = (super_method(object, variable) || default_value)
25
+
26
+ if instance_var.class == Class
27
+ object.instance_variable_set("@#{variable}", instance_var)
28
+ else
29
+ object.instance_variable_set("@#{variable}", duplicate(instance_var))
30
+ end
31
+ end
32
+
33
+ def stringify_keys(hash)
34
+ stringified_hash = {}
35
+ hash.keys.each { |key| stringified_hash[key.to_s] = hash[key] }
36
+ stringified_hash
37
+ end
38
+
39
+ def symbolyze_keys(hash)
40
+ hash.keys.reduce({}) do |cloned_hash, key|
41
+ cloned_hash[key.to_sym] = hash[key]
42
+ cloned_hash
43
+ end
44
+ end
45
+
46
+ def plural?(string)
47
+ string = string.to_s
48
+ string == string.pluralize
49
+ end
50
+
51
+ def duplicate(object)
52
+ object.dup rescue object
53
+ end
54
+
55
+ def blank?(object)
56
+ case object
57
+ when String
58
+ object.to_s == ''
59
+ when Array
60
+ object.empty?
61
+ else
62
+ object.nil?
63
+ end
64
+ end
65
+
66
+ def present?(object)
67
+ !blank?(object)
68
+ end
69
+
70
+ def absolute_path?(string)
71
+ present?(string) && string[0] == '/'
72
+ end
73
+
74
+ def remove_initial_slash(string)
75
+ string[1..-1]
76
+ end
77
+
78
+ end
79
+ end
@@ -0,0 +1,81 @@
1
+ module SmoothOperator
2
+ module ModelSchema
3
+
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ def known_attribute?(attribute)
9
+ known_attributes.include?(attribute.to_s)
10
+ end
11
+
12
+ def known_by_schema?(attribute)
13
+ self.class.internal_structure.include?(attribute.to_s)
14
+ end
15
+
16
+ def known_attributes
17
+ @known_attributes ||= self.class.known_attributes.dup
18
+ end
19
+
20
+ module ClassMethods
21
+
22
+ def resources_name(default_bypass = nil)
23
+ return @resources_name if defined?(@resources_name)
24
+
25
+ (Helpers.super_method(self, :resources_name, true) || (default_bypass ? nil : self.resource_name.pluralize))
26
+ end
27
+ attr_writer :resources_name
28
+
29
+ def resource_name(default_bypass = nil)
30
+ return @resource_name if defined?(@resource_name)
31
+
32
+ (Helpers.super_method(self, :resource_name, true) || (default_bypass ? nil : self.model_name.to_s.underscore))
33
+ end
34
+ attr_writer :resource_name
35
+
36
+ def schema(structure)
37
+ internal_structure.merge! Helpers.stringify_keys(structure)
38
+
39
+ known_attributes.merge internal_structure.keys
40
+ end
41
+
42
+ def internal_structure
43
+ Helpers.get_instance_variable(self, :internal_structure, { "errors" => nil })
44
+ end
45
+
46
+ def known_attributes
47
+ Helpers.get_instance_variable(self, :known_attributes, Set.new)
48
+ end
49
+
50
+ def model_name
51
+ return '' if @_model_name == :none
52
+
53
+ if defined? ActiveModel
54
+ rails_model_name_method
55
+ else
56
+ @_model_name ||= name.split('::').last.underscore.capitalize
57
+ end
58
+ end
59
+
60
+ def model_name=(name)
61
+ @_model_name = name
62
+ end
63
+
64
+ protected ############## PROTECTED #############
65
+
66
+ def rails_model_name_method
67
+ @model_name ||= begin
68
+ namespace ||= self.parents.detect do |n|
69
+ n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
70
+ end
71
+
72
+ ActiveModel::Name.new(self, namespace, @_model_name).tap do |model_name|
73
+ def model_name.human(options = {}); @klass.send(:_translate, "models.#{i18n_key}", options); end
74
+ end
75
+ end
76
+ end
77
+
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,37 @@
1
+ require "smooth_operator/delegation"
2
+ require "smooth_operator/validations"
3
+ require "smooth_operator/model_schema"
4
+ require "smooth_operator/serialization"
5
+ require "smooth_operator/attribute_methods"
6
+ require "smooth_operator/attribute_assignment"
7
+
8
+ module SmoothOperator
9
+ module OpenStruct
10
+
11
+ class Base
12
+
13
+ include Delegation
14
+ include Validations
15
+ include ModelSchema
16
+ include Serialization
17
+ include AttributeMethods
18
+ include AttributeAssignment
19
+
20
+ def self.strict_behaviour=(value)
21
+ @strict_behaviour = value
22
+ end
23
+
24
+ def self.strict_behaviour
25
+ Helpers.get_instance_variable(self, :strict_behaviour, false)
26
+ end
27
+
28
+ end
29
+
30
+ class Dirty < Base
31
+
32
+ dirty_attributes
33
+
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,145 @@
1
+ require "smooth_operator/remote_call/base"
2
+ require "smooth_operator/operators/faraday"
3
+ require "smooth_operator/operators/typhoeus"
4
+ require "smooth_operator/remote_call/errors/timeout"
5
+ require "smooth_operator/remote_call/errors/connection_failed"
6
+
7
+ module SmoothOperator
8
+
9
+ module Operator
10
+
11
+ def make_the_call(http_verb, relative_path = '', data = {}, options = {})
12
+ options ||= {}
13
+
14
+ relative_path = resource_path(relative_path)
15
+
16
+ if !parent_object.nil? && options[:ignore_parent] != true
17
+ options[:resources_name] ||= "#{parent_object.class.resources_name}/#{parent_object.get_primary_key}/#{self.class.resources_name}"
18
+ end
19
+
20
+ self.class.make_the_call(http_verb, relative_path, data, options) do |remote_call|
21
+ yield(remote_call)
22
+ end
23
+ end
24
+
25
+ protected ######################## PROTECTED ###################
26
+
27
+ def resource_path(relative_path)
28
+ if Helpers.absolute_path?(relative_path)
29
+ Helpers.remove_initial_slash(relative_path)
30
+ elsif persisted?
31
+ Helpers.present?(relative_path) ? "#{get_primary_key}/#{relative_path}" : get_primary_key.to_s
32
+ else
33
+ relative_path
34
+ end
35
+ end
36
+
37
+
38
+ ########################### MODULES BELLOW ###############################
39
+
40
+ module HttpMethods
41
+
42
+ HTTP_VERBS = %w[get post put patch delete]
43
+
44
+ HTTP_VERBS.each do |method|
45
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
46
+ def #{method}(relative_path = '', params = {}, options = {})
47
+ make_the_call(:#{method}, relative_path, params, options) do |remote_call|
48
+ block_given? ? yield(remote_call) : remote_call
49
+ end
50
+ end
51
+ RUBY
52
+ end
53
+
54
+ end
55
+
56
+ module ClassMethods
57
+
58
+ OPTIONS = [:endpoint, :endpoint_user, :endpoint_pass, :timeout]
59
+
60
+ OPTIONS.each do |option|
61
+ define_method(option) { Helpers.get_instance_variable(self, option, '') }
62
+ end
63
+
64
+ attr_writer *OPTIONS
65
+
66
+ def headers
67
+ Helpers.get_instance_variable(self, :headers, {})
68
+ end
69
+
70
+ attr_writer :headers
71
+
72
+
73
+ def make_the_call(http_verb, relative_path = '', data = {}, options = {})
74
+ operator_args = operator_method_args(http_verb, relative_path, data, options)
75
+
76
+ if Helpers.present?(operator_args[4][:hydra])
77
+ operator_call = Operators::Typhoeus
78
+ else
79
+ operator_call = Operators::Faraday
80
+ end
81
+
82
+ operator_call.make_the_call(*operator_args) do |remote_call|
83
+ block_given? ? yield(remote_call) : remote_call
84
+ end
85
+ end
86
+
87
+ def query_string(params)
88
+ params
89
+ end
90
+
91
+
92
+ protected #################### PROTECTED ##################
93
+
94
+ def operator_method_args(http_verb, relative_path, data, options)
95
+ options = populate_options(options)
96
+
97
+ [http_verb, resource_path(relative_path, options), *strip_params(http_verb, data), options]
98
+ end
99
+
100
+
101
+ private #################### PRIVATE ##################
102
+
103
+ def populate_options(options)
104
+ options ||= {}
105
+
106
+ OPTIONS.each { |option| options[option] ||= send(option) }
107
+
108
+ options[:headers] = headers.merge(options[:headers] || {})
109
+
110
+ options
111
+ end
112
+
113
+ def resource_path(relative_path, options)
114
+ _resources_name = options[:resources_name] || self.resources_name
115
+
116
+ if Helpers.present?(_resources_name)
117
+ Helpers.present?(relative_path) ? "#{_resources_name}/#{relative_path}" : _resources_name
118
+ else
119
+ relative_path.to_s
120
+ end
121
+ end
122
+
123
+ def strip_params(http_verb, data)
124
+ data ||= {}
125
+
126
+ if [:get, :head, :delete].include?(http_verb)
127
+ [query_string(data), nil]
128
+ else
129
+ [query_string({}), data]
130
+ end
131
+ end
132
+
133
+ end
134
+
135
+
136
+ include HttpMethods
137
+
138
+ def self.included(base)
139
+ base.extend(ClassMethods)
140
+ base.extend(HttpMethods)
141
+ end
142
+
143
+ end
144
+
145
+ end
@@ -0,0 +1,75 @@
1
+ require 'faraday'
2
+ require 'typhoeus/adapters/faraday'
3
+ require "smooth_operator/remote_call/faraday"
4
+
5
+ module SmoothOperator
6
+
7
+ module Operators
8
+
9
+ module Faraday
10
+
11
+ extend self
12
+
13
+ # def generate_parallel_connection
14
+ # generate_connection(:typhoeus)
15
+ # end
16
+
17
+ def generate_connection(adapter = nil, options = nil)
18
+ adapter ||= :net_http
19
+
20
+ ::Faraday.new(url: options[:endpoint]) do |builder|
21
+ builder.options[:timeout] = options[:timeout].to_i unless Helpers.blank?(options[:timeout])
22
+ builder.request :url_encoded
23
+ builder.adapter adapter
24
+ end
25
+ end
26
+
27
+ def make_the_call(http_verb, resource_path, params, body, options)
28
+ connection, request_options, options = strip_options(options)
29
+
30
+ remote_call = begin
31
+ set_basic_authentication(connection, options)
32
+
33
+ response = connection.send(http_verb, resource_path) do |request|
34
+ request_configuration(request, request_options, options, params, body)
35
+ end
36
+
37
+ RemoteCall::Faraday.new(response)
38
+ rescue ::Faraday::Error::ConnectionFailed
39
+ RemoteCall::Errors::ConnectionFailed.new(response)
40
+ rescue ::Faraday::Error::TimeoutError
41
+ RemoteCall::Errors::Timeout.new(response)
42
+ end
43
+
44
+ block_given? ? yield(remote_call) : remote_call
45
+ end
46
+
47
+
48
+ protected ################ PROTECTED ################
49
+
50
+ def strip_options(options)
51
+ request_options = options.delete(:request_options) || {}
52
+
53
+ connection = options.delete(:connection) || generate_connection(nil, options)
54
+
55
+ [connection, request_options, options]
56
+ end
57
+
58
+ def set_basic_authentication(connection, options)
59
+ connection.basic_auth(options[:endpoint_user], options[:endpoint_pass]) if Helpers.present?(options[:endpoint_user])
60
+ end
61
+
62
+ def request_configuration(request, request_options, options, params, body)
63
+ request_options.each { |key, value| request.options.send("#{key}=", value) }
64
+
65
+ options[:headers].each { |key, value| request.headers[key] = value }
66
+
67
+ params.each { |key, value| request.params[key] = value }
68
+
69
+ request.body = body
70
+ end
71
+
72
+ end
73
+
74
+ end
75
+ end