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,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
module EntityConcern
|
5
|
+
# An ActiveRecord-like attribute management feature, with the exception
|
6
|
+
# that the attributes are not generated through a schema file, but are
|
7
|
+
# defined inline the entity class.
|
8
|
+
#
|
9
|
+
module Attributes
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
included do
|
13
|
+
# Include all custom typed attribute helpers
|
14
|
+
include Attributes::DateArray
|
15
|
+
include Attributes::Enum
|
16
|
+
include Attributes::Range
|
17
|
+
include Attributes::StringInquirer
|
18
|
+
|
19
|
+
# Collect all registed attribute names as symbols
|
20
|
+
class_attribute :attribute_names
|
21
|
+
|
22
|
+
# Export all attributes as data hash, as requested by the ActiveModel
|
23
|
+
# API.
|
24
|
+
#
|
25
|
+
# @param sanitize [Boolean] whenever to sanitize the data for transport
|
26
|
+
# @return [Hash{String => Mixed}] the attribute data
|
27
|
+
#
|
28
|
+
# rubocop:disable Metrics/MethodLength because of the
|
29
|
+
# key/value sanitization handling
|
30
|
+
def attributes(sanitize = false)
|
31
|
+
attribute_names.each_with_object({}) do |key, memo|
|
32
|
+
reader = key
|
33
|
+
|
34
|
+
if sanitize
|
35
|
+
sanitizer = "sanitize_attr_#{key}".to_sym
|
36
|
+
reader = methods.include?(sanitizer) ? sanitizer : key
|
37
|
+
|
38
|
+
key_sanitizer = "sanitize_attr_key_#{key}".to_sym
|
39
|
+
key = methods.include?(key_sanitizer) ? send(key_sanitizer) : key
|
40
|
+
end
|
41
|
+
|
42
|
+
result = resolve_attributes(send(reader))
|
43
|
+
memo[key.to_s] = result
|
44
|
+
end
|
45
|
+
end
|
46
|
+
# rubocop:enable Metrics/MethodLength
|
47
|
+
|
48
|
+
# A wrapper for the +ActiveModel#assign_attributes+ method with support
|
49
|
+
# for unmapped attributes. These attributes are put into the
|
50
|
+
# +_unmapped+ struct and all the known attributes are assigned like
|
51
|
+
# normal. This allows the client to be forward compatible with changing
|
52
|
+
# APIs.
|
53
|
+
#
|
54
|
+
# @param struct [Hash{Mixed => Mixed}, RecursiveOpenStruct] the data
|
55
|
+
# to assign
|
56
|
+
# @return [Mixed] the input data which was assigned
|
57
|
+
def assign_attributes(struct = {})
|
58
|
+
# Build a RecursiveOpenStruct and a simple hash from the given data
|
59
|
+
struct, hash = sanitize_data(struct)
|
60
|
+
# Initialize associations and map them accordingly
|
61
|
+
struct, hash = initialize_associations(struct, hash)
|
62
|
+
# Initialize attributes and map unknown ones and pass back the known
|
63
|
+
known = initialize_attributes(struct, hash)
|
64
|
+
# Mass assign the known attributes via ActiveModel
|
65
|
+
super(known)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
# Resolve the attributes for the given object while respecting
|
71
|
+
# sanitization and deep arrays.
|
72
|
+
#
|
73
|
+
# @param obj [Mixed] the object to resolve its attributes
|
74
|
+
# @param sanitize [Boolean] whenever to sanitize the data for transport
|
75
|
+
# @return [Mixed] the attribute(s) data
|
76
|
+
def resolve_attributes(obj, sanitize = false)
|
77
|
+
if obj.respond_to? :attributes
|
78
|
+
obj = if obj.method(:attributes).arity == 1
|
79
|
+
obj.attributes(sanitize)
|
80
|
+
else
|
81
|
+
obj.attributes
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
obj = obj.map { |elem| resolve_attributes(elem, sanitize) } \
|
86
|
+
if obj.is_a? Array
|
87
|
+
|
88
|
+
obj
|
89
|
+
end
|
90
|
+
|
91
|
+
# Explicitly convert the given struct to an +RecursiveOpenStruct+ and a
|
92
|
+
# deep symbolized key copy for further usage.
|
93
|
+
#
|
94
|
+
# @param data [Hash{Mixed => Mixed}, RecursiveOpenStruct] the initial
|
95
|
+
# data
|
96
|
+
# @return [Array<RecursiveOpenStruct, Hash{Symbol => Mixed}>] the
|
97
|
+
# left over data
|
98
|
+
def sanitize_data(data = {})
|
99
|
+
# Convert the given arguments to a recursive open struct,
|
100
|
+
# when not already done
|
101
|
+
data = ::RecursiveOpenStruct.new(data, recurse_over_arrays: true) \
|
102
|
+
unless data.is_a? ::RecursiveOpenStruct
|
103
|
+
# Symbolize all keys in deep (including hashes in arrays), while
|
104
|
+
# converting back to an ordinary hash
|
105
|
+
[data, data.to_h]
|
106
|
+
end
|
107
|
+
|
108
|
+
# Process the given data by separating the known from the unknown
|
109
|
+
# attributes. The unknown attributes are collected on the +_unmapped+
|
110
|
+
# variable for later access. This allows the entities to be
|
111
|
+
# forward-compatible on the application HTTP responses in case of
|
112
|
+
# additions.
|
113
|
+
#
|
114
|
+
# @param struct [RecursiveOpenStruct] all the data as struct
|
115
|
+
# @param hash [Hash{Symbol => Mixed}] all the data as hash
|
116
|
+
# @return [Hash{Symbol => Mixed}] the known attributes
|
117
|
+
def initialize_attributes(struct, hash)
|
118
|
+
# Substract known keys, to move them to the +_unmapped+ variable
|
119
|
+
attribute_names.each { |key| struct.delete_field(key) }
|
120
|
+
# Merge the previous unmapped struct and the given data
|
121
|
+
self._unmapped = \
|
122
|
+
::RecursiveOpenStruct.new(_unmapped.to_h.merge(struct.to_h),
|
123
|
+
recurse_over_arrays: true)
|
124
|
+
# Allow mass assignment of known attributes
|
125
|
+
hash.slice(*attribute_names)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
class_methods do
|
130
|
+
# Initialize the attributes structures on an inherited class.
|
131
|
+
#
|
132
|
+
# @param child_class [Class] the child class which inherits us
|
133
|
+
def inherited_setup_attributes(child_class)
|
134
|
+
child_class.attribute_names = []
|
135
|
+
end
|
136
|
+
|
137
|
+
# Register tracked attributes of the entity. This adds the attributes
|
138
|
+
# to the +Class.attribute_names+ collection, generates getters and
|
139
|
+
# setters as well sets up the dirty-tracking of these attributes.
|
140
|
+
#
|
141
|
+
# @param args [Array<Symbol>] the attributes to register
|
142
|
+
def tracked_attr(*args)
|
143
|
+
# Register the attribute names, for easy access
|
144
|
+
self.attribute_names += args
|
145
|
+
# Define getters/setters
|
146
|
+
attr_reader(*args)
|
147
|
+
args.each do |arg|
|
148
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
149
|
+
def #{arg}=(value)
|
150
|
+
#{arg}_will_change!
|
151
|
+
@#{arg} = value
|
152
|
+
end
|
153
|
+
RUBY
|
154
|
+
end
|
155
|
+
# Register the attributes for ActiveModel
|
156
|
+
define_attribute_methods(*args)
|
157
|
+
end
|
158
|
+
|
159
|
+
# (Re-)Register attributes with strict type casts. This adds additional
|
160
|
+
# reader methods as well as a writer with casted type.
|
161
|
+
#
|
162
|
+
# @param name [Symbol, String] the name of the attribute
|
163
|
+
# @param type [Symbol] the type of the attribute
|
164
|
+
# @param args [Hash{Symbol => Mixed}] additional options for the type
|
165
|
+
def typed_attr(name, type, **args)
|
166
|
+
send("typed_attr_#{type}", name, **args)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
module EntityConcern
|
5
|
+
# Define all the base callbacks of a common entity.
|
6
|
+
module Callbacks
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
class_methods do
|
10
|
+
include ActiveModel::Callbacks
|
11
|
+
end
|
12
|
+
|
13
|
+
included do
|
14
|
+
# Define all the base callbacks
|
15
|
+
define_model_callbacks :initialize, only: :after
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
module EntityConcern
|
5
|
+
# Allow entities to define their low level HTTP client to use.
|
6
|
+
module Client
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
# The class constant of the low level client
|
11
|
+
class_attribute :client_class
|
12
|
+
|
13
|
+
# Get the cached low level client instance.
|
14
|
+
#
|
15
|
+
# @return [Mixed] the client instance
|
16
|
+
def client
|
17
|
+
@client ||= client_class.new
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class_methods do
|
22
|
+
# Allows an entity to configure the client it depends on.
|
23
|
+
#
|
24
|
+
# @param name [Symbol, String] the client name, in snake_case
|
25
|
+
def client(name)
|
26
|
+
self.client_class = PriceHubble.client(name).class
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
# The common PriceHubble coordinates object.
|
5
|
+
#
|
6
|
+
# @see https://docs.pricehubble.com/#types-coordinates
|
7
|
+
class Coordinates < BaseEntity
|
8
|
+
# Mapped and tracked attributes
|
9
|
+
tracked_attr :latitude, :longitude
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
# The common PriceHubble location object.
|
5
|
+
#
|
6
|
+
# @see https://docs.pricehubble.com/#types-location
|
7
|
+
class Location < BaseEntity
|
8
|
+
# Associations
|
9
|
+
with_options(initialize: true, persist: true) do
|
10
|
+
has_one :address
|
11
|
+
has_one :coordinates
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
# The common PriceHubble property object.
|
5
|
+
#
|
6
|
+
# @see https://docs.pricehubble.com/#types-property
|
7
|
+
class Property < BaseEntity
|
8
|
+
# All valid condition values
|
9
|
+
CONDITIONS = %i[renovation_needed well_maintained
|
10
|
+
new_or_recently_renovated].freeze
|
11
|
+
# All valid quality values
|
12
|
+
QUALITIES = %i[simple normal high_quality luxury].freeze
|
13
|
+
|
14
|
+
# Mapped and tracked attributes
|
15
|
+
tracked_attr :location, :property_type, :building_year, :living_area,
|
16
|
+
:land_area, :garden_area, :volume, :number_of_rooms,
|
17
|
+
:number_of_bathrooms, :balcony_area,
|
18
|
+
:number_of_indoor_parking_spaces,
|
19
|
+
:number_of_outdoor_parking_spaces, :floor_number, :has_lift,
|
20
|
+
:energy_label, :has_sauna, :has_pool,
|
21
|
+
:number_of_floors_in_building, :is_furnished, :is_new,
|
22
|
+
:renovation_year
|
23
|
+
|
24
|
+
# Associations
|
25
|
+
with_options(initialize: true, persist: true) do
|
26
|
+
has_one :location
|
27
|
+
has_one :property_type
|
28
|
+
has_one :condition, class_name: PropertyConditions
|
29
|
+
has_one :quality, class_name: PropertyQualities
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
# The nested PriceHubble property condition object.
|
5
|
+
#
|
6
|
+
# @see https://docs.pricehubble.com/#types-property
|
7
|
+
class PropertyConditions < BaseEntity
|
8
|
+
# Mapped and tracked attributes
|
9
|
+
tracked_attr :bathrooms, :kitchen, :flooring, :windows, :masonry
|
10
|
+
|
11
|
+
# Define attribute types for casting
|
12
|
+
with_options(values: Property::CONDITIONS) do
|
13
|
+
typed_attr :bathrooms, :enum
|
14
|
+
typed_attr :kitchen, :enum
|
15
|
+
typed_attr :flooring, :enum
|
16
|
+
typed_attr :windows, :enum
|
17
|
+
typed_attr :masonry, :enum
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
# The nested PriceHubble property quality object.
|
5
|
+
#
|
6
|
+
# @see https://docs.pricehubble.com/#types-property
|
7
|
+
class PropertyQualities < BaseEntity
|
8
|
+
# Mapped and tracked attributes
|
9
|
+
tracked_attr :bathrooms, :kitchen, :flooring, :windows, :masonry
|
10
|
+
|
11
|
+
# Define attribute types for casting
|
12
|
+
with_options(values: Property::QUALITIES) do
|
13
|
+
typed_attr :bathrooms, :enum
|
14
|
+
typed_attr :kitchen, :enum
|
15
|
+
typed_attr :flooring, :enum
|
16
|
+
typed_attr :windows, :enum
|
17
|
+
typed_attr :masonry, :enum
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
# The common PriceHubble property type object.
|
5
|
+
#
|
6
|
+
# @see https://docs.pricehubble.com/#types-propertytype
|
7
|
+
class PropertyType < BaseEntity
|
8
|
+
# Mapped and tracked attributes
|
9
|
+
tracked_attr :code, :subcode
|
10
|
+
|
11
|
+
# Define attribute types for casting
|
12
|
+
typed_attr :code, :enum, values: %i[apartment house]
|
13
|
+
typed_attr :subcode, :enum, values: %i[apartment_normal
|
14
|
+
apartment_maisonette apartment_attic
|
15
|
+
apartment_penthouse
|
16
|
+
apartment_terraced apartment_studio
|
17
|
+
house_detached house_semi_detached
|
18
|
+
house_row_corner house_row_middle
|
19
|
+
house_farm]
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
# The PriceHubble valuation result for a given property.
|
5
|
+
#
|
6
|
+
# @see https://docs.pricehubble.com/#international-valuation
|
7
|
+
class Valuation < BaseEntity
|
8
|
+
# Mapped and tracked attributes
|
9
|
+
tracked_attr :currency, :sale_price, :sale_price_range, :rent_gross,
|
10
|
+
:rent_gross_range, :rent_net, :rent_net_range, :confidence,
|
11
|
+
:scores, :status, :deal_type, :valuation_date, :country_code
|
12
|
+
|
13
|
+
# Define attribute types for casting
|
14
|
+
typed_attr :currency, :string_inquirer
|
15
|
+
typed_attr :confidence, :string_inquirer
|
16
|
+
typed_attr :deal_type, :string_inquirer
|
17
|
+
typed_attr :country_code, :string_inquirer
|
18
|
+
typed_attr :sale_price_range, :range
|
19
|
+
typed_attr :rent_gross_range, :range
|
20
|
+
typed_attr :rent_net_range, :range
|
21
|
+
|
22
|
+
# Associations
|
23
|
+
with_options(persist: true, initialize: true) do
|
24
|
+
has_one :property
|
25
|
+
has_one :scores, class_name: ValuationScores
|
26
|
+
end
|
27
|
+
|
28
|
+
# A streamlined helper to get the value by the deal type. (sale price
|
29
|
+
# if deal type is +sale+ or gross rent value if deal type is +rent+)
|
30
|
+
#
|
31
|
+
# @return [Integer, nil] the value of the property
|
32
|
+
def value
|
33
|
+
return sale_price if deal_type.sale?
|
34
|
+
|
35
|
+
rent_gross
|
36
|
+
end
|
37
|
+
|
38
|
+
# A streamlined helper to get the value range by the deal type. (sale price
|
39
|
+
# range if deal type is +sale+ or gross rent range if deal type is +rent+)
|
40
|
+
#
|
41
|
+
# @return [Range, nil] the value range of the property
|
42
|
+
def value_range
|
43
|
+
return sale_price_range if deal_type.sale?
|
44
|
+
|
45
|
+
rent_gross_range
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
# The PriceHubble valuation request for one or more properties.
|
5
|
+
#
|
6
|
+
# @see https://docs.pricehubble.com/#international-valuation
|
7
|
+
class ValuationRequest < BaseEntity
|
8
|
+
# Configure the client to use
|
9
|
+
client :valuation
|
10
|
+
|
11
|
+
# Mapped and tracked attributes
|
12
|
+
tracked_attr :deal_type, :valuation_dates, :properties, :return_scores,
|
13
|
+
:country_code
|
14
|
+
|
15
|
+
# Define attribute types for casting
|
16
|
+
typed_attr :deal_type, :enum, values: %i[sale rent]
|
17
|
+
typed_attr :valuation_dates, :date_array
|
18
|
+
typed_attr :country_code, :string_inquirer
|
19
|
+
|
20
|
+
# Associations
|
21
|
+
has_many :properties, fallback_from: :property,
|
22
|
+
persist: true,
|
23
|
+
initialize: true
|
24
|
+
|
25
|
+
# Set some defaults when initialized
|
26
|
+
after_initialize do
|
27
|
+
self.deal_type ||= :sale
|
28
|
+
self.valuation_dates ||= [Date.current]
|
29
|
+
self.return_scores = false if return_scores.nil?
|
30
|
+
self.country_code ||= 'DE'
|
31
|
+
end
|
32
|
+
|
33
|
+
# For transportation the +properties+ array name must be changed to reflect
|
34
|
+
# the actual API specification.
|
35
|
+
#
|
36
|
+
# @return [Symbol] the sanitized name of the properties array
|
37
|
+
def sanitize_attr_key_properties
|
38
|
+
:valuation_inputs
|
39
|
+
end
|
40
|
+
|
41
|
+
# For transportation the +properties+ array must be sanitized
|
42
|
+
# to reflect the actual API specification.
|
43
|
+
#
|
44
|
+
# @return [Array<Hash{String => Mixed}>] the sanitized properties
|
45
|
+
def sanitize_attr_properties
|
46
|
+
properties.map { |prop| { property: prop.attributes(true) } }
|
47
|
+
end
|
48
|
+
|
49
|
+
# Perform the property valuation request.
|
50
|
+
#
|
51
|
+
# @param args [Hash{Symbol => Mixed}] additional options
|
52
|
+
# @return [Array<PriceHubble::Valuation>] the valuation results
|
53
|
+
def perform(**args)
|
54
|
+
client.property_value(self, **args) || []
|
55
|
+
end
|
56
|
+
|
57
|
+
# Generate bang method variants
|
58
|
+
bangers :perform
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
# The nested PriceHubble valuation scores object.
|
5
|
+
#
|
6
|
+
# @see https://docs.pricehubble.com/#international-valuation
|
7
|
+
class ValuationScores < BaseEntity
|
8
|
+
# Mapped and tracked attributes
|
9
|
+
tracked_attr :quality, :condition, :location
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
# Generic entity exception class.
|
5
|
+
class EntityError < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
# Generic request/response exception class.
|
9
|
+
class RequestError < StandardError
|
10
|
+
attr_reader :response
|
11
|
+
|
12
|
+
# Create a new instance of the error.
|
13
|
+
#
|
14
|
+
# @param message [String] the error message
|
15
|
+
# @param response [Faraday::Response] the response
|
16
|
+
def initialize(message = nil, response = nil)
|
17
|
+
@response = response
|
18
|
+
message ||= response.body.message if response.body.respond_to? :message
|
19
|
+
|
20
|
+
super(message)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Raised when the authentication request failed.
|
25
|
+
class AuthenticationError < RequestError; end
|
26
|
+
|
27
|
+
# Raised when an entity was not found while searching/getting.
|
28
|
+
class EntityNotFound < EntityError
|
29
|
+
attr_reader :entity, :criteria
|
30
|
+
|
31
|
+
# Create a new instance of the error.
|
32
|
+
#
|
33
|
+
# @param message [String] the error message
|
34
|
+
# @param entity [PriceHubble::BaseEntity] the entity which was not found
|
35
|
+
# @param criteria [Hash{Symbol => Mixed}] the search/find criteria
|
36
|
+
def initialize(message = nil, entity = nil, criteria = {})
|
37
|
+
@entity = entity
|
38
|
+
@criteria = criteria
|
39
|
+
message ||= "Couldn't find #{entity} with #{criteria.inspect}"
|
40
|
+
|
41
|
+
super(message)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Raised when the record is invalid, due to a response.
|
46
|
+
class EntityInvalid < EntityError
|
47
|
+
attr_reader :entity
|
48
|
+
|
49
|
+
# Create a new instance of the error.
|
50
|
+
#
|
51
|
+
# @param message [String] the error message
|
52
|
+
# @param entity [PriceHubble::BaseEntity] the entity which was invalid
|
53
|
+
def initialize(message = nil, entity = nil)
|
54
|
+
@entity = entity
|
55
|
+
message ||= "Invalid #{entity}"
|
56
|
+
|
57
|
+
super(message)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Register our custom faraday middlewares
|
4
|
+
Faraday::Request.register_middleware \
|
5
|
+
ph_data_sanitization: PriceHubble::Client::Request::DataSanitization
|
6
|
+
Faraday::Request.register_middleware \
|
7
|
+
ph_default_headers: PriceHubble::Client::Request::DefaultHeaders
|
8
|
+
|
9
|
+
Faraday::Response.register_middleware \
|
10
|
+
ph_data_sanitization: PriceHubble::Client::Response::DataSanitization
|
11
|
+
Faraday::Response.register_middleware \
|
12
|
+
ph_recursive_open_struct: PriceHubble::Client::Response::RecursiveOpenStruct
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
# Handles all the identity retrival high-level logic.
|
5
|
+
#
|
6
|
+
# rubocop:disable Style/ClassVars because we split module code
|
7
|
+
module Identity
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
class_methods do
|
10
|
+
# Reset the current identity.
|
11
|
+
def reset_identity!
|
12
|
+
@@identity = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
# Get the current identity we use for all requests. We try to
|
16
|
+
# authenticate against the PriceHubble Authentication API with the
|
17
|
+
# configured credentials from the Gem configuration.
|
18
|
+
# (+PriceHubble.configuration+) In case this went well, we cache the
|
19
|
+
# result. Otherwise we raise an +AuthenticationError+.
|
20
|
+
#
|
21
|
+
# @return [PriceHubble::Authentication] the authentication instance
|
22
|
+
# @raise [AuthenticationError] in case of a failed login
|
23
|
+
def identity
|
24
|
+
# Fetch a new identity with the configured identity settings from the
|
25
|
+
# Gem configuration
|
26
|
+
@@identity ||= auth_by_config
|
27
|
+
# Take care of an expired identity
|
28
|
+
@@identity = auth_by_config if @@identity.expired?
|
29
|
+
# Pass back the actual identity instance
|
30
|
+
@@identity
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# Perform the authentication via the configured identity credentials.
|
36
|
+
#
|
37
|
+
# @return [Hausgold::Jwt] the new JWT instance
|
38
|
+
# @raise [AuthenticationError] in case of a failed login
|
39
|
+
def auth_by_config
|
40
|
+
args = identity_params.dup
|
41
|
+
client(:authentication).login!(**args)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
# rubocop:enable Style/ClassVars
|
46
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
# Rails-specific initializations.
|
5
|
+
class Railtie < Rails::Railtie
|
6
|
+
# Run before all Rails initializers, but after the application is defined
|
7
|
+
config.before_initialize do
|
8
|
+
# Nothing to do here at the moment.
|
9
|
+
end
|
10
|
+
|
11
|
+
# Run after all configuration is set via Rails initializers
|
12
|
+
config.after_initialize do
|
13
|
+
# Nothing to do here at the moment.
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
module Utils
|
5
|
+
# Generate bang variants of methods which use the +Decision+ flow control.
|
6
|
+
module Bangers
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
class_methods do
|
10
|
+
# Generate bang variants for the given methods.
|
11
|
+
# Be sure to use the +bangers+ class method AFTER all method
|
12
|
+
# definitions, otherwise it will raise errors about missing methods.
|
13
|
+
#
|
14
|
+
# @param methods [Array<Symbol>] the source method names
|
15
|
+
# @raise [NoMethodError] when a source method is not defined
|
16
|
+
# @raise [ArgumentError] when a source method does not accept arguments
|
17
|
+
#
|
18
|
+
# rubocop:disable Metrics/MethodLength because the method template
|
19
|
+
# is better inlined
|
20
|
+
def bangers(*methods)
|
21
|
+
methods.each do |meth|
|
22
|
+
raise NoMethodError, "#{self}##{meth} does not exit" \
|
23
|
+
unless instance_methods(false).include? meth
|
24
|
+
|
25
|
+
raise ArgumentError, "#{self}##{meth} does not accept arguments" \
|
26
|
+
if instance_method(meth).arity.zero?
|
27
|
+
|
28
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
29
|
+
def #{meth}!(*args)
|
30
|
+
if args.last.is_a? Hash
|
31
|
+
args.last.merge!(bang: true)
|
32
|
+
else
|
33
|
+
args.push({ bang: true })
|
34
|
+
end
|
35
|
+
#{meth}(*args)
|
36
|
+
end
|
37
|
+
RUBY
|
38
|
+
end
|
39
|
+
end
|
40
|
+
# rubocop:enable Metrics/MethodLength
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|