pricehubble 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +30 -0
  3. data/.gitignore +16 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +62 -0
  6. data/.simplecov +3 -0
  7. data/.travis.yml +27 -0
  8. data/.yardopts +6 -0
  9. data/Appraisals +25 -0
  10. data/CHANGELOG.md +9 -0
  11. data/Dockerfile +29 -0
  12. data/Envfile +6 -0
  13. data/Gemfile +8 -0
  14. data/LICENSE +21 -0
  15. data/Makefile +149 -0
  16. data/README.md +385 -0
  17. data/Rakefile +80 -0
  18. data/bin/console +16 -0
  19. data/bin/run +12 -0
  20. data/bin/setup +8 -0
  21. data/config/docker/.bash_profile +3 -0
  22. data/config/docker/.bashrc +48 -0
  23. data/config/docker/.inputrc +17 -0
  24. data/doc/assets/project.svg +68 -0
  25. data/doc/examples/authentication.rb +19 -0
  26. data/doc/examples/complex_property_valuations.rb +91 -0
  27. data/doc/examples/config.rb +30 -0
  28. data/doc/examples/property_valuations_errors.rb +30 -0
  29. data/doc/examples/simple_property_valuations.rb +69 -0
  30. data/docker-compose.yml +9 -0
  31. data/gemfiles/rails_4.2.gemfile +11 -0
  32. data/gemfiles/rails_5.0.gemfile +11 -0
  33. data/gemfiles/rails_5.1.gemfile +11 -0
  34. data/gemfiles/rails_5.2.gemfile +11 -0
  35. data/lib/price_hubble.rb +3 -0
  36. data/lib/pricehubble/client/authentication.rb +29 -0
  37. data/lib/pricehubble/client/base.rb +59 -0
  38. data/lib/pricehubble/client/request/data_sanitization.rb +28 -0
  39. data/lib/pricehubble/client/request/default_headers.rb +33 -0
  40. data/lib/pricehubble/client/response/data_sanitization.rb +29 -0
  41. data/lib/pricehubble/client/response/recursive_open_struct.rb +31 -0
  42. data/lib/pricehubble/client/utils/request.rb +41 -0
  43. data/lib/pricehubble/client/utils/response.rb +60 -0
  44. data/lib/pricehubble/client/valuation.rb +68 -0
  45. data/lib/pricehubble/client.rb +25 -0
  46. data/lib/pricehubble/configuration.rb +26 -0
  47. data/lib/pricehubble/configuration_handling.rb +50 -0
  48. data/lib/pricehubble/core_ext/hash.rb +52 -0
  49. data/lib/pricehubble/entity/address.rb +11 -0
  50. data/lib/pricehubble/entity/authentication.rb +34 -0
  51. data/lib/pricehubble/entity/base_entity.rb +63 -0
  52. data/lib/pricehubble/entity/concern/associations.rb +197 -0
  53. data/lib/pricehubble/entity/concern/attributes/date_array.rb +29 -0
  54. data/lib/pricehubble/entity/concern/attributes/enum.rb +57 -0
  55. data/lib/pricehubble/entity/concern/attributes/range.rb +32 -0
  56. data/lib/pricehubble/entity/concern/attributes/string_inquirer.rb +27 -0
  57. data/lib/pricehubble/entity/concern/attributes.rb +171 -0
  58. data/lib/pricehubble/entity/concern/callbacks.rb +19 -0
  59. data/lib/pricehubble/entity/concern/client.rb +31 -0
  60. data/lib/pricehubble/entity/coordinates.rb +11 -0
  61. data/lib/pricehubble/entity/location.rb +14 -0
  62. data/lib/pricehubble/entity/property.rb +32 -0
  63. data/lib/pricehubble/entity/property_conditions.rb +20 -0
  64. data/lib/pricehubble/entity/property_qualities.rb +20 -0
  65. data/lib/pricehubble/entity/property_type.rb +21 -0
  66. data/lib/pricehubble/entity/valuation.rb +48 -0
  67. data/lib/pricehubble/entity/valuation_request.rb +60 -0
  68. data/lib/pricehubble/entity/valuation_scores.rb +11 -0
  69. data/lib/pricehubble/errors.rb +60 -0
  70. data/lib/pricehubble/faraday.rb +12 -0
  71. data/lib/pricehubble/identity.rb +46 -0
  72. data/lib/pricehubble/railtie.rb +16 -0
  73. data/lib/pricehubble/utils/bangers.rb +44 -0
  74. data/lib/pricehubble/utils/decision.rb +97 -0
  75. data/lib/pricehubble/version.rb +6 -0
  76. data/lib/pricehubble.rb +103 -0
  77. data/pricehubble.gemspec +47 -0
  78. metadata +432 -0
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PriceHubble
4
+ # A bunch of top-level client helpers.
5
+ module Client
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ # Get a low level client for the requested application. This returns an
10
+ # already instanciated client object, ready to use.
11
+ #
12
+ # @param name [Symbol, String] the client name
13
+ # @return [PriceHubble::Client::Base] a compatible client instance
14
+ def client(name)
15
+ name
16
+ .to_s
17
+ .underscore
18
+ .camelize
19
+ .prepend('PriceHubble::Client::')
20
+ .constantize
21
+ .new
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PriceHubble
4
+ # The configuration for the pricehubble gem.
5
+ class Configuration
6
+ include ActiveSupport::Configurable
7
+
8
+ # Configure the API authentication credentials
9
+ config_accessor(:username) { ENV.fetch('PRICEHUBBLE_USERNAME', nil) }
10
+ config_accessor(:password) { ENV.fetch('PRICEHUBBLE_PASSWORD', nil) }
11
+
12
+ # The base URL of the API (there is no staging/canary
13
+ # endpoint we know about)
14
+ config_accessor(:base_url) do
15
+ val = ENV.fetch('PRICEHUBBLE_BASE_URL', 'https://api.pricehubble.com')
16
+ val.strip.chomp('/')
17
+ end
18
+
19
+ # The logger instance to use (when available we use the +Rails.logger+)
20
+ config_accessor(:logger) do
21
+ next(Rails.logger) if defined? Rails
22
+
23
+ Logger.new($stdout)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PriceHubble
4
+ # The top-level configuration handling.
5
+ #
6
+ # rubocop:disable Style/ClassVars because we split module code
7
+ module ConfigurationHandling
8
+ extend ActiveSupport::Concern
9
+
10
+ class_methods do
11
+ # Retrieve the current configuration object.
12
+ #
13
+ # @return [Configuration] the current configuration object
14
+ def configuration
15
+ @@configuration ||= Configuration.new
16
+ end
17
+
18
+ # Configure the concern by providing a block which takes
19
+ # care of this task. Example:
20
+ #
21
+ # FactoryBot::Instrumentation.configure do |conf|
22
+ # # conf.xyz = [..]
23
+ # end
24
+ def configure
25
+ yield(configuration)
26
+ end
27
+
28
+ # Reset the current configuration with the default one.
29
+ def reset_configuration!
30
+ @@configuration = Configuration.new
31
+ end
32
+
33
+ # Get back the credentials bundle for an authentication.
34
+ #
35
+ # @return [Hash{Symbol => String}] the identity bundle
36
+ def identity_params
37
+ {
38
+ username: configuration.username,
39
+ password: configuration.password
40
+ }
41
+ end
42
+
43
+ # Retrieve the current configured logger instance.
44
+ #
45
+ # @return [Logger] the logger instance
46
+ delegate :logger, to: :configuration
47
+ end
48
+ end
49
+ # rubocop:enable Style/ClassVars
50
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ # All our Ruby core extensions for the +Hash+ class.
4
+ class Hash
5
+ # Perform the regular +Hash#compact+ method on the object but takes care of
6
+ # deeply nested hashs.
7
+ #
8
+ # @return [Hash]
9
+ def deep_compact
10
+ deep_compact_in_object(self)
11
+ end
12
+
13
+ # Perform a deep key transformation on the hash,
14
+ # so all keys are in camelcase.
15
+ #
16
+ # @return [Hash]
17
+ def deep_camelize_keys
18
+ deep_transform_keys { |key| key.to_s.camelize(:lower) }
19
+ end
20
+
21
+ # Perform a deep key transformation on the hash,
22
+ # so all keys are in snakecase/underscored.
23
+ #
24
+ # @return [Hash]
25
+ def deep_underscore_keys
26
+ deep_transform_keys { |key| key.to_s.underscore }
27
+ end
28
+
29
+ private
30
+
31
+ # A supporting helper to allow deep hash compaction.
32
+ #
33
+ # @param object [Mixed] the object to compact
34
+ # @return [Mixed] the compacted object
35
+ #
36
+ # rubocop:disable Metrics/MethodLength because of the extra empty
37
+ # hash compaction logic
38
+ def deep_compact_in_object(object)
39
+ case object
40
+ when Hash
41
+ object = object.compact.each_with_object({}) do |(key, value), result|
42
+ result[key] = deep_compact_in_object(value)
43
+ end
44
+ object.empty? ? nil : object.compact
45
+ when Array
46
+ object.map { |item| deep_compact_in_object(item) }
47
+ else
48
+ object
49
+ end
50
+ end
51
+ # rubocop:enable Metrics/MethodLength
52
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PriceHubble
4
+ # The common PriceHubble address object.
5
+ #
6
+ # @see https://docs.pricehubble.com/#types-address
7
+ class Address < BaseEntity
8
+ # Mapped and tracked attributes
9
+ tracked_attr :post_code, :city, :street, :house_number
10
+ end
11
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PriceHubble
4
+ # The PriceHubble ecosystem is built around the authentication on each
5
+ # request. This entity provides a simple to use interface for it.
6
+ #
7
+ # @see https://docs.pricehubble.com/#introduction-authentication
8
+ class Authentication < BaseEntity
9
+ # The expiration leeway to substract to guarantee
10
+ # acceptance on remote application calls
11
+ EXPIRATION_LEEWAY = 5.minutes
12
+
13
+ # Mapped and tracked attributes
14
+ tracked_attr :access_token, :expires_in
15
+
16
+ # Add some runtime attributes
17
+ attr_reader :created_at, :expires_at
18
+
19
+ # Register the time of initializing as base for the expiration
20
+ after_initialize do
21
+ @created_at = Time.current
22
+ @expires_at = created_at + (expires_in || 1.hour.to_i) - EXPIRATION_LEEWAY
23
+ end
24
+
25
+ # Allow to query whenever the current authentication instance is expired or
26
+ # not. This includes also a small leeway to ensure the acceptance is
27
+ # guaranteed.
28
+ #
29
+ # @return [Boolean] whenever the authentication instance is expired or not
30
+ def expired?
31
+ Time.current >= expires_at
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PriceHubble
4
+ # The base entity, with a lot of known ActiveRecord/ActiveModel features.
5
+ class BaseEntity
6
+ include ActiveModel::Model
7
+ include ActiveModel::Dirty
8
+
9
+ include PriceHubble::Utils::Bangers
10
+
11
+ # Additional singleton class functionalities
12
+ class << self
13
+ include PriceHubble::Utils::Bangers
14
+ end
15
+
16
+ include PriceHubble::EntityConcern::Callbacks
17
+ include PriceHubble::EntityConcern::Attributes
18
+ include PriceHubble::EntityConcern::Associations
19
+ include PriceHubble::EntityConcern::Client
20
+
21
+ # We collect all unknown attributes instead of raising while creating a new
22
+ # instance. The unknown attributes are wrapped inside a
23
+ # +RecursiveOpenStruct+ to ease the accessibility. This also allows us to
24
+ # handle responses in a forward-compatible way.
25
+ attr_accessor :_unmapped
26
+
27
+ # Create a new instance of an entity with a lot of known
28
+ # ActiveRecord/ActiveModel features.
29
+ #
30
+ # @param struct [Hash{Mixed => Mixed}, RecursiveOpenStruct] the initial data
31
+ # @return [PriceHubble::BaseEntity] a compatible instance
32
+ # @yield [PriceHubble::BaseEntity] the entity itself in the end
33
+ def initialize(struct = {})
34
+ # Set the initial unmapped struct
35
+ self._unmapped = RecursiveOpenStruct.new
36
+ # Build a RecursiveOpenStruct and a simple hash from the given data
37
+ struct, hash = sanitize_data(struct)
38
+ # Initialize associations and map them accordingly
39
+ struct, hash = initialize_associations(struct, hash)
40
+ # Initialize attributes and map unknown ones and pass back the known
41
+ known = initialize_attributes(struct, hash)
42
+ # Mass assign the known attributes via ActiveModel
43
+ super(known)
44
+ # Follow the ActiveRecord API
45
+ yield self if block_given?
46
+ # Run the initializer callbacks
47
+ _run_initialize_callbacks
48
+ end
49
+
50
+ class << self
51
+ # Initialize the class we were inherited to. We trigger all our methods
52
+ # which start with +inherited_setup_+ to allow per-concern/feature based
53
+ # initialization after BaseEntity inheritance.
54
+ #
55
+ # @param child_class [Class] the child class which inherits us
56
+ def inherited(child_class)
57
+ match = ->(sym) { sym.to_s.start_with? 'inherited_setup_' }
58
+ trigger = ->(sym) { send(sym, child_class) }
59
+ methods.select(&match).each(&trigger)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PriceHubble
4
+ module EntityConcern
5
+ # Allow simple association mappings like ActiveRecord supports (eg.
6
+ # +has_one+, +has_many+).
7
+ module Associations
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ # Collect all the registed association configurations
12
+ class_attribute :associations
13
+
14
+ private
15
+
16
+ # Map the registered associations to the actual instance.
17
+ #
18
+ # @param struct [RecursiveOpenStruct] all the data as struct
19
+ # @param hash [Hash{Symbol => Mixed}] all the data as hash
20
+ # @return [Array<RecursiveOpenStruct, Hash{Symbol => Mixed}>] the
21
+ # left over data
22
+ def initialize_associations(struct, hash)
23
+ # Walk through the configured associations and set them up
24
+ associations.each_with_object([struct, hash]) do |cur, memo|
25
+ opts = cur.last
26
+ send("map_#{opts[:type]}_association", cur.first, opts, *memo)
27
+ end
28
+ end
29
+
30
+ # Map an simple has_one association to the resulting entity attribute.
31
+ # The source key is stripped off according to the association
32
+ # definition.
33
+ #
34
+ # @param attribute [Symbol] the name of the destination attribute
35
+ # @param opts [Hash{Symbol => Mixed}] the association definition
36
+ # @param struct [RecursiveOpenStruct] all the data as struct
37
+ # @param hash [Hash{Symbol => Mixed}] all the data as hash
38
+ # @return [Array<RecursiveOpenStruct, Hash{Symbol => Mixed}>] the
39
+ # left over data
40
+ #
41
+ # rubocop:disable Metrics/AbcSize because of the complex logic
42
+ # rubocop:disable Metrics/MethodLength because of the complex logic
43
+ def map_has_one_association(attribute, opts, struct, hash)
44
+ # Early exit when the source key is missing on the given data
45
+ key = opts[:from]
46
+
47
+ # Initialize an object on the association, even without data
48
+ if opts[:initialize] && send(attribute).nil?
49
+ hash[key] = {} unless hash.key? key
50
+ end
51
+
52
+ return [struct, hash] unless hash.key? key
53
+
54
+ # Instantiate a new entity from the association
55
+ val = hash[key]
56
+ val = opts[:class_name].new(val) unless val.is_a? opts[:class_name]
57
+ send("#{attribute}=", val)
58
+
59
+ # Strip off the source key, because we mapped it
60
+ struct.delete_field(key)
61
+ hash = hash.delete(key)
62
+ # Pass back the new data
63
+ [struct, hash]
64
+ end
65
+ # rubocop:enable Metrics/AbcSize
66
+ # rubocop:enable Metrics/MethodLength
67
+
68
+ # Map an simple has_many association to the resulting entity attribute.
69
+ # The source key is stripped off according to the association
70
+ # definition. Each element from the source attribute is instantiated
71
+ # separate and is added to the destination collection.
72
+ #
73
+ # @param attribute [Symbol] the name of the destination attribute
74
+ # @param opts [Hash{Symbol => Mixed}] the association definition
75
+ # @param struct [RecursiveOpenStruct] all the data as struct
76
+ # @param hash [Hash{Symbol => Mixed}] all the data as hash
77
+ # @return [Array<RecursiveOpenStruct, Hash{Symbol => Mixed}>] the
78
+ # left over data
79
+ #
80
+ # rubocop:disable Metrics/AbcSize because of the complex logic
81
+ # rubocop:disable Metrics/CyclomaticComplexity because of the
82
+ # complex logic
83
+ # rubocop:disable Metrics/PerceivedComplexity because of the
84
+ # complex logic
85
+ # rubocop:disable Metrics/MethodLength because of the complex logic
86
+ def map_has_many_association(attribute, opts, struct, hash)
87
+ # Early exit when the source key is missing on the given data
88
+ key = opts[:from]
89
+
90
+ # When a singular appender was configured, we allow to use it as
91
+ # attribute source if the regular source is not available
92
+ key = opts[:fallback_from] if opts[:fallback_from] && !hash.key?(key)
93
+
94
+ # Initialize an empty array on the association
95
+ if opts[:initialize] && send(attribute).nil?
96
+ hash[key] = [] unless hash.key? key
97
+ end
98
+
99
+ return [struct, hash] unless hash.key? key
100
+
101
+ # Instantiate a new entity from each association element
102
+ hash[key] = [hash[key]] unless hash[key].is_a? Array
103
+ collection = hash[key].map do |elem|
104
+ next elem if elem.is_a? opts[:class_name]
105
+
106
+ opts[:class_name].new(elem)
107
+ end
108
+ send("#{attribute}=", collection)
109
+
110
+ # Strip off the source key, because we mapped it
111
+ struct.delete_field(key)
112
+ hash = hash.delete(key)
113
+
114
+ # Pass back the new data
115
+ [struct, hash]
116
+ end
117
+ # rubocop:enable Metrics/AbcSize
118
+ # rubocop:enable Metrics/CyclomaticComplexity
119
+ # rubocop:enable Metrics/PerceivedComplexity
120
+ # rubocop:enable Metrics/MethodLength
121
+ end
122
+
123
+ # rubocop:disable Naming/PredicateName because we follow
124
+ # known naming conventions
125
+ class_methods do
126
+ # Initialize the associations structures on an inherited class.
127
+ #
128
+ # @param child_class [Class] the child class which inherits us
129
+ def inherited_setup_associations(child_class)
130
+ child_class.associations = {}
131
+ end
132
+
133
+ # Define a simple +has_one+ association.
134
+ #
135
+ # Options
136
+ # * +:class_name+ - the entity class to use, otherwise it is guessed
137
+ # * +:from+ - take the data from this attribute
138
+ # * +:persist+ - whenever to send the association
139
+ # attributes (default: false)
140
+ # * +:initialize+ - whenever to initialize an empty object
141
+ #
142
+ # @param entity [String, Symbol] the attribute/entity name
143
+ # @param args [Hash{Symbol => Mixed}] additional options
144
+ def has_one(entity, **args)
145
+ # Sanitize options
146
+ entity = entity.to_sym
147
+ opts = { class_name: nil, from: entity, persist: false } \
148
+ .merge(args).merge(type: :has_one)
149
+ # Resolve the given entity to a class name, when no explicit class
150
+ # name was given via options
151
+ if opts[:class_name].nil?
152
+ opts[:class_name] = \
153
+ entity.to_s.camelcase.prepend('PriceHubble::').constantize
154
+ end
155
+ # Register the association
156
+ associations[entity] = opts
157
+ # Generate getters and setters
158
+ attr_accessor entity
159
+ # Add the entity to the tracked attributes if it should be persisted
160
+ tracked_attr entity if opts[:persist]
161
+ end
162
+
163
+ # Define a simple +has_many+ association.
164
+ #
165
+ # Options
166
+ # * +:class_name+ - the entity class to use, otherwise it is guessed
167
+ # * +:from+ - take the data from this attribute
168
+ # * +:fallback_from+ - otherwise take the data from the fallback
169
+ # * +:persist+ - whenever to send the association
170
+ # attributes (default: false)
171
+ # * +:initialize+ - whenever to initialize an empty array
172
+ #
173
+ # @param entity [String, Symbol] the attribute/entity name
174
+ # @param args [Hash{Symbol => Mixed}] additional options
175
+ def has_many(entity, **args)
176
+ # Sanitize options
177
+ entity = entity.to_sym
178
+ opts = { class_name: nil, from: entity, persist: false } \
179
+ .merge(args).merge(type: :has_many)
180
+ # Resolve the given entity to a class name, when no explicit class
181
+ # name was given via options
182
+ if opts[:class_name].nil?
183
+ opts[:class_name] = entity.to_s.singularize.camelcase
184
+ .prepend('PriceHubble::').constantize
185
+ end
186
+ # Register the association
187
+ associations[entity] = opts
188
+ # Generate getters and setters
189
+ attr_accessor entity
190
+ # Add the entity to the tracked attributes if it should be persisted
191
+ tracked_attr entity if opts[:persist]
192
+ end
193
+ end
194
+ # rubocop:enable Naming/PredicateName
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PriceHubble
4
+ module EntityConcern
5
+ module Attributes
6
+ # A separated date array typed attribute helper.
7
+ module DateArray
8
+ extend ActiveSupport::Concern
9
+
10
+ class_methods do
11
+ # Register a date array attribute (only sanitization).
12
+ #
13
+ # @param name [Symbol, String] the name of the attribute
14
+ # @param _args [Hash{Symbol => Mixed}] additional options
15
+ def typed_attr_date_array(name, **_args)
16
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
17
+ def sanitize_attr_#{name}
18
+ return if @#{name}.nil?
19
+ @#{name}.map do |date|
20
+ date.strftime('%Y-%m-%d')
21
+ end
22
+ end
23
+ RUBY
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PriceHubble
4
+ module EntityConcern
5
+ module Attributes
6
+ # A separated enum typed attribute helper.
7
+ module Enum
8
+ extend ActiveSupport::Concern
9
+
10
+ class_methods do
11
+ # Register a fixed enum attribute.
12
+ #
13
+ # @param name [Symbol, String] the name of the attribute
14
+ # @param values [Array<String, Symbol>] the allowed values
15
+ # @param _args [Hash{Symbol => Mixed}] additional options
16
+ #
17
+ # rubocop:disable Metrics/MethodLength because of the inline
18
+ # meta method definitions
19
+ def typed_attr_enum(name, values:, **_args)
20
+ values = values.map(&:to_sym)
21
+ const_values = "ATTR_#{name.to_s.upcase}"
22
+
23
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
24
+ # Define the constant for all valid values
25
+ #{const_values} = #{values}.freeze
26
+
27
+ def #{name}=(value)
28
+ #{name}_will_change!
29
+ value = value.to_sym
30
+
31
+ unless #{const_values}.include? value
32
+ raise ArgumentError, "'\#{value}' is not a valid #{name} " \
33
+ "(values: \#{#{const_values}})"
34
+ end
35
+
36
+ @#{name} = value
37
+ end
38
+ RUBY
39
+
40
+ values.each do |value|
41
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
42
+ def #{value}!
43
+ self.#{name} = :#{value}
44
+ end
45
+
46
+ def #{value}?
47
+ self.#{name} == :#{value}
48
+ end
49
+ RUBY
50
+ end
51
+ end
52
+ # rubocop:enable Metrics/MethodLength
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PriceHubble
4
+ module EntityConcern
5
+ module Attributes
6
+ # A separated range typed attribute helper.
7
+ module Range
8
+ extend ActiveSupport::Concern
9
+
10
+ class_methods do
11
+ # Register a range attribute.
12
+ #
13
+ # @param name [Symbol, String] the name of the attribute
14
+ # @param _args [Hash{Symbol => Mixed}] additional options
15
+ def typed_attr_range(name, **_args)
16
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
17
+ def #{name}
18
+ # We cannot handle nil values
19
+ return unless @#{name}
20
+
21
+ # Otherwise we assume the hash contains a +lower+ and +upper+
22
+ # key from which we assemble the range
23
+ hash = @#{name}
24
+ hash[:lower]..hash[:upper]
25
+ end
26
+ RUBY
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PriceHubble
4
+ module EntityConcern
5
+ module Attributes
6
+ # A separated string inquirer typed attribute helper.
7
+ module StringInquirer
8
+ extend ActiveSupport::Concern
9
+
10
+ class_methods do
11
+ # Register a casted string inquirer attribute.
12
+ #
13
+ # @param name [Symbol, String] the name of the attribute
14
+ # @param _args [Hash{Symbol => Mixed}] additional options
15
+ def typed_attr_string_inquirer(name, **_args)
16
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
17
+ def #{name}=(value)
18
+ #{name}_will_change!
19
+ @#{name} = ActiveSupport::StringInquirer.new(value.to_s)
20
+ end
21
+ RUBY
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end