pricehubble 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +30 -0
- data/.gitignore +16 -0
- data/.rspec +3 -0
- data/.rubocop.yml +62 -0
- data/.simplecov +3 -0
- data/.travis.yml +27 -0
- data/.yardopts +6 -0
- data/Appraisals +25 -0
- data/CHANGELOG.md +9 -0
- data/Dockerfile +29 -0
- data/Envfile +6 -0
- data/Gemfile +8 -0
- data/LICENSE +21 -0
- data/Makefile +149 -0
- data/README.md +385 -0
- data/Rakefile +80 -0
- data/bin/console +16 -0
- data/bin/run +12 -0
- data/bin/setup +8 -0
- data/config/docker/.bash_profile +3 -0
- data/config/docker/.bashrc +48 -0
- data/config/docker/.inputrc +17 -0
- data/doc/assets/project.svg +68 -0
- data/doc/examples/authentication.rb +19 -0
- data/doc/examples/complex_property_valuations.rb +91 -0
- data/doc/examples/config.rb +30 -0
- data/doc/examples/property_valuations_errors.rb +30 -0
- data/doc/examples/simple_property_valuations.rb +69 -0
- data/docker-compose.yml +9 -0
- data/gemfiles/rails_4.2.gemfile +11 -0
- data/gemfiles/rails_5.0.gemfile +11 -0
- data/gemfiles/rails_5.1.gemfile +11 -0
- data/gemfiles/rails_5.2.gemfile +11 -0
- data/lib/price_hubble.rb +3 -0
- data/lib/pricehubble/client/authentication.rb +29 -0
- data/lib/pricehubble/client/base.rb +59 -0
- data/lib/pricehubble/client/request/data_sanitization.rb +28 -0
- data/lib/pricehubble/client/request/default_headers.rb +33 -0
- data/lib/pricehubble/client/response/data_sanitization.rb +29 -0
- data/lib/pricehubble/client/response/recursive_open_struct.rb +31 -0
- data/lib/pricehubble/client/utils/request.rb +41 -0
- data/lib/pricehubble/client/utils/response.rb +60 -0
- data/lib/pricehubble/client/valuation.rb +68 -0
- data/lib/pricehubble/client.rb +25 -0
- data/lib/pricehubble/configuration.rb +26 -0
- data/lib/pricehubble/configuration_handling.rb +50 -0
- data/lib/pricehubble/core_ext/hash.rb +52 -0
- data/lib/pricehubble/entity/address.rb +11 -0
- data/lib/pricehubble/entity/authentication.rb +34 -0
- data/lib/pricehubble/entity/base_entity.rb +63 -0
- data/lib/pricehubble/entity/concern/associations.rb +197 -0
- data/lib/pricehubble/entity/concern/attributes/date_array.rb +29 -0
- data/lib/pricehubble/entity/concern/attributes/enum.rb +57 -0
- data/lib/pricehubble/entity/concern/attributes/range.rb +32 -0
- data/lib/pricehubble/entity/concern/attributes/string_inquirer.rb +27 -0
- data/lib/pricehubble/entity/concern/attributes.rb +171 -0
- data/lib/pricehubble/entity/concern/callbacks.rb +19 -0
- data/lib/pricehubble/entity/concern/client.rb +31 -0
- data/lib/pricehubble/entity/coordinates.rb +11 -0
- data/lib/pricehubble/entity/location.rb +14 -0
- data/lib/pricehubble/entity/property.rb +32 -0
- data/lib/pricehubble/entity/property_conditions.rb +20 -0
- data/lib/pricehubble/entity/property_qualities.rb +20 -0
- data/lib/pricehubble/entity/property_type.rb +21 -0
- data/lib/pricehubble/entity/valuation.rb +48 -0
- data/lib/pricehubble/entity/valuation_request.rb +60 -0
- data/lib/pricehubble/entity/valuation_scores.rb +11 -0
- data/lib/pricehubble/errors.rb +60 -0
- data/lib/pricehubble/faraday.rb +12 -0
- data/lib/pricehubble/identity.rb +46 -0
- data/lib/pricehubble/railtie.rb +16 -0
- data/lib/pricehubble/utils/bangers.rb +44 -0
- data/lib/pricehubble/utils/decision.rb +97 -0
- data/lib/pricehubble/version.rb +6 -0
- data/lib/pricehubble.rb +103 -0
- data/pricehubble.gemspec +47 -0
- 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
|