smooth_operator 0.4.4 → 1.2.0

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