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,91 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative './config'
5
+
6
+ # Build a property with all relevant information for the valuation. This
7
+ # example reflects not the full list of supported fields, [see the API
8
+ # specification](https://docs.pricehubble.com/#types-property) for the full
9
+ # details.
10
+ apartment = PriceHubble::Property.new(
11
+ location: {
12
+ address: {
13
+ post_code: '22769',
14
+ city: 'Hamburg',
15
+ street: 'Stresemannstr.',
16
+ house_number: '29'
17
+ }
18
+ },
19
+ property_type: { code: :apartment },
20
+ building_year: 1990,
21
+ living_area: 200,
22
+ balcony_Area: 30,
23
+ floor_number: 5,
24
+ has_lift: true,
25
+ is_furnished: false,
26
+ is_new: false,
27
+ renovation_year: 2014,
28
+ condition: {
29
+ bathrooms: :well_maintained,
30
+ kitchen: :well_maintained,
31
+ flooring: :well_maintained,
32
+ windows: :well_maintained,
33
+ masonry: :well_maintained
34
+ },
35
+ quality: {
36
+ bathrooms: :normal,
37
+ kitchen: :normal,
38
+ flooring: :normal,
39
+ windows: :normal,
40
+ masonry: :normal
41
+ }
42
+ )
43
+
44
+ house = PriceHubble::Property.new(
45
+ location: {
46
+ address: {
47
+ post_code: '22769',
48
+ city: 'Hamburg',
49
+ street: 'Stresemannstr.',
50
+ house_number: '29'
51
+ }
52
+ },
53
+ property_type: { code: :house },
54
+ building_year: 1990,
55
+ land_area: 100,
56
+ living_area: 500,
57
+ number_of_floors_in_building: 5
58
+ )
59
+
60
+ # Fetch the property valuations for multiple properties, on multiple dates
61
+ request = PriceHubble::ValuationRequest.new(
62
+ deal_type: :sale,
63
+ properties: [apartment, house],
64
+ # The dates order is reflected on the valuations list
65
+ valuation_dates: [
66
+ 1.year.ago,
67
+ Date.current,
68
+ 1.year.from_now
69
+ ]
70
+ )
71
+ valuations = request.perform!
72
+
73
+ # Print the valuations in a simple ASCII table. This is just a
74
+ # demonstration of how to use the valuations for a simple usecase.
75
+ require 'terminal-table'
76
+ table = Terminal::Table.new do |tab|
77
+ tab << ['Deal Type', 'Property Type', *request.valuation_dates.map(&:year)]
78
+ tab << :separator
79
+ # Group the valuations by the property they represent
80
+ valuations.group_by(&:property).each do |property, valuations|
81
+ tab << [request.deal_type, property.property_type.code,
82
+ *valuations.map { |val| "#{val.value} #{val.currency}" }]
83
+ end
84
+ end
85
+ puts table
86
+ # => +-----------+---------------+-------------+-------------+-------------+
87
+ # => | Deal Type | Property Type | 2018 | 2019 | 2020 |
88
+ # => +-----------+---------------+-------------+-------------+-------------+
89
+ # => | sale | apartment | 1282100 EUR | 1373100 EUR | 1420100 EUR |
90
+ # => | sale | house | 1824800 EUR | 1950900 EUR | 2016000 EUR |
91
+ # => +-----------+---------------+-------------+-------------+-------------+
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'pricehubble'
5
+
6
+ PriceHubble.reset_configuration!
7
+ PriceHubble.configure do |conf|
8
+ # conf.request_logging = true
9
+ end
10
+
11
+ errors = false
12
+
13
+ if ENV['PRICEHUBBLE_USERNAME'].blank?
14
+ errors = true
15
+ puts '> [ERR] Environment variable `PRICEHUBBLE_USERNAME` is missing.'
16
+ end
17
+
18
+ if ENV['PRICEHUBBLE_PASSWORD'].blank?
19
+ errors = true
20
+ puts '> [ERR] Environment variable `PRICEHUBBLE_PASSWORD` is missing.'
21
+ end
22
+
23
+ if errors
24
+ puts
25
+ puts '> Usage:'
26
+ puts "> $ export PRICEHUBBLE_USERNAME='your-username'"
27
+ puts "> $ export PRICEHUBBLE_PASSWORD='your-password'"
28
+ puts "> $ #{$PROGRAM_NAME} #{ARGV.join(' ')}"
29
+ exit
30
+ end
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative './config'
5
+
6
+ begin
7
+ # Fetch the valuations for a single property (sale) for today (defaults).
8
+ # This is the bare minimum variant, as a starting point.
9
+ PriceHubble::ValuationRequest.new(
10
+ property: {
11
+ location: {
12
+ address: {
13
+ post_code: '22769',
14
+ city: 'Hamburg',
15
+ street: 'Stresemannstr.',
16
+ house_number: '29'
17
+ }
18
+ },
19
+ property_type: { code: :apartment },
20
+ building_year: 2999,
21
+ living_area: 200
22
+ }
23
+ ).perform!
24
+ rescue PriceHubble::EntityInvalid => e
25
+ # => #<PriceHubble::EntityInvalid: buildingYear: ...>
26
+
27
+ # The error message includes the detailed problem.
28
+ pp e.message
29
+ # => "buildingYear: Must be between 1850 and 2022."
30
+ end
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative './config'
5
+
6
+ # Fetch the valuations for a single property (sale) for today (defaults).
7
+ # This is the bare minimum variant, as a starting point.
8
+ valuations = PriceHubble::ValuationRequest.new(
9
+ property: {
10
+ location: {
11
+ address: {
12
+ post_code: '22769',
13
+ city: 'Hamburg',
14
+ street: 'Stresemannstr.',
15
+ house_number: '29'
16
+ }
17
+ },
18
+ property_type: { code: :apartment },
19
+ building_year: 1990,
20
+ living_area: 200
21
+ }
22
+ ).perform!
23
+ # => [#<PriceHubble::Valuation ...>]
24
+
25
+ # Print all relevant valuation attributes
26
+ pp valuations.first.attributes.deep_compact
27
+ # => {"currency"=>"EUR",
28
+ # => "sale_price"=>1283400,
29
+ # => "sale_price_range"=>1180800..1386100,
30
+ # => "confidence"=>"good",
31
+ # => "deal_type"=>"sale",
32
+ # => "valuation_date"=>Thu, 17 Oct 2019,
33
+ # => "country_code"=>"DE",
34
+ # => "property"=>
35
+ # => {"location"=>
36
+ # => {"address"=>
37
+ # => {"post_code"=>"22769",
38
+ # => "city"=>"Hamburg",
39
+ # => "street"=>"Stresemannstr.",
40
+ # => "house_number"=>"29"}},
41
+ # => "property_type"=>{"code"=>:apartment},
42
+ # => "building_year"=>1990,
43
+ # => "living_area"=>200}}
44
+
45
+ # We just want to work on the first valuation
46
+ valuation = valuations.first
47
+
48
+ # Get the deal type dependent value of the property in a generic way.
49
+ # (sale price if deal type is sale, and rent gross if deal type is rent)
50
+ pp valuation.value
51
+ # => 1283400
52
+
53
+ # Get the upper and lower value range of the property in a generic
54
+ # way. The deal type logic is equal to the
55
+ # +PriceHubble::Valuation#value+ method.
56
+ pp valuation.value_range
57
+ # => 1180800..1386100
58
+
59
+ # Query the valuation confidence in an elegant way
60
+ pp valuation.confidence.good?
61
+ # => true
62
+
63
+ # The +PriceHubble::Valuation+ entity is a self contained
64
+ # representation of the request and response. This means it includes
65
+ # the property, deal type, valuation date, country code, etc it was
66
+ # requested for. This makes it easy to process the data because
67
+ # everything related is in one place.
68
+ pp valuation.property.property_type.apartment?
69
+ # => true
@@ -0,0 +1,9 @@
1
+ version: "3"
2
+ services:
3
+ test:
4
+ build: .
5
+ env_file: Envfile
6
+ network_mode: bridge
7
+ working_dir: /app
8
+ volumes:
9
+ - .:/app
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file was generated by Appraisal
4
+
5
+ source 'https://rubygems.org'
6
+
7
+ gem 'activemodel', '~> 4.2.11'
8
+ gem 'activesupport', '~> 4.2.11'
9
+ gem 'railties', '~> 4.2.11'
10
+
11
+ gemspec path: '../'
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file was generated by Appraisal
4
+
5
+ source 'https://rubygems.org'
6
+
7
+ gem 'activemodel', '~> 5.0.7'
8
+ gem 'activesupport', '~> 5.0.7'
9
+ gem 'railties', '~> 5.0.7'
10
+
11
+ gemspec path: '../'
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file was generated by Appraisal
4
+
5
+ source 'https://rubygems.org'
6
+
7
+ gem 'activemodel', '~> 5.1.6'
8
+ gem 'activesupport', '~> 5.1.6'
9
+ gem 'railties', '~> 5.1.6'
10
+
11
+ gemspec path: '../'
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file was generated by Appraisal
4
+
5
+ source 'https://rubygems.org'
6
+
7
+ gem 'activemodel', '~> 5.2.2'
8
+ gem 'activesupport', '~> 5.2.2'
9
+ gem 'railties', '~> 5.2.2'
10
+
11
+ gemspec path: '../'
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pricehubble'
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PriceHubble
4
+ module Client
5
+ # A high level client library for the PriceHubble Authentication API.
6
+ class Authentication < Base
7
+ # Perform a login request while sending the passed credentials.
8
+ #
9
+ # @param args [Hash{Symbol => Mixed}] the authentication credentials
10
+ # @return [PriceHubble::Authentication, nil] the authentication entity,
11
+ # or +nil+ on error
12
+ def login(**args)
13
+ res = connection.post do |req|
14
+ req.path = '/auth/login/credentials'
15
+ req.body = args.except(:bang)
16
+ use_default_context(req, :login)
17
+ end
18
+ decision(bang: args.fetch(:bang, false)) do |result|
19
+ result.bang { PriceHubble::AuthenticationError.new(nil, res) }
20
+ result.good { PriceHubble::Authentication.new(res.body) }
21
+ successful?(res)
22
+ end
23
+ end
24
+
25
+ # Generate bang method variants
26
+ bangers :login
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PriceHubble
4
+ module Client
5
+ # The base API client class definition. It bundles all the separated
6
+ # application logic on a low level.
7
+ class Base
8
+ include PriceHubble::Client::Utils::Request
9
+ include PriceHubble::Client::Utils::Response
10
+ include PriceHubble::Utils::Decision
11
+ include PriceHubble::Utils::Bangers
12
+
13
+ # Configure the connection instance in a generic manner. Each client can
14
+ # modify the connection in a specific way, when the application requires
15
+ # special handling. Just overwrite the +configure+ method, and call
16
+ # +super(con)+. Here is a full example:
17
+ #
18
+ # def configure(con)
19
+ # super(con)
20
+ # con.request :url_encoded
21
+ # con.response :logger
22
+ # con.adapter Faraday.default_adapter
23
+ # end
24
+ #
25
+ # @param con [Faraday::Connection] the connection object
26
+ #
27
+ # rubocop:disable Metrics/MethodLength because of the middleware list
28
+ def configure(con)
29
+ con.use :instrumentation
30
+
31
+ # The definition order is execution order
32
+ con.request :ph_data_sanitization
33
+ con.request :ph_default_headers
34
+ con.request :json
35
+ con.request :multipart
36
+ con.request :url_encoded
37
+
38
+ # The definition order is reverse to the execution order
39
+ con.response :ph_recursive_open_struct
40
+ con.response :ph_data_sanitization
41
+ con.response :dates
42
+ con.response :json, content_type: /\bjson$/
43
+ con.response :follow_redirects
44
+
45
+ con.adapter Faraday.default_adapter
46
+ end
47
+ # rubocop:enable Metrics/MethodLength
48
+
49
+ # Create a new Faraday connection on the first shot, and pass the cached
50
+ # connection object on subsequent calls.
51
+ #
52
+ # @return [Faraday::Connection] the connection object
53
+ def connection
54
+ @connection ||= Faraday.new(url: PriceHubble.configuration.base_url,
55
+ &method(:configure))
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PriceHubble
4
+ module Client
5
+ module Request
6
+ # We like snake cased attributes here, but the API requires camel cased
7
+ # hash keys, so we take care of it in a generic way here. Furthermore, we
8
+ # perform a deep compaction of the given body hash, to strip +nil+ values
9
+ # and empty hashes from the request. There is no need to send null-data
10
+ # on a round trip.
11
+ class DataSanitization < Faraday::Middleware
12
+ # Serve the Faraday middleware API.
13
+ #
14
+ # @param env [Hash{Symbol => Mixed}] the request
15
+ def call(env)
16
+ body = env[:body]
17
+
18
+ # Perform the data compaction and the hash key transformation,
19
+ # when the body is available and a hash
20
+ env[:body] = body.deep_compact.deep_camelize_keys \
21
+ if body&.is_a?(Hash)
22
+
23
+ @app.call(env)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PriceHubble
4
+ module Client
5
+ module Request
6
+ # Add some default headers to every request.
7
+ class DefaultHeaders < Faraday::Middleware
8
+ # Serve the Faraday middleware API.
9
+ #
10
+ # @param env [Hash{Symbol => Mixed}] the request
11
+ def call(env)
12
+ env[:request_headers].merge! \
13
+ 'User-Agent' => user_agent,
14
+ 'Accept' => 'application/json'
15
+
16
+ # Set request content type to JSON as fallback
17
+ env[:request_headers]['Content-Type'] = 'application/json' \
18
+ if env[:request_headers]['Content-Type'].blank?
19
+
20
+ @app.call(env)
21
+ end
22
+
23
+ # Build an useful user agent string to pass. We identify ourself as
24
+ # +PriceHubbleGem+ with the current gem version.
25
+ #
26
+ # @return [String] the user agent string
27
+ def user_agent
28
+ "PriceHubbleGem/#{PriceHubble::VERSION}"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PriceHubble
4
+ module Client
5
+ module Response
6
+ # We like snake cased attributes here, but the API sends camel cased
7
+ # hash keys on responses, so we take care of it in a generic way here.
8
+ class DataSanitization < Faraday::Middleware
9
+ # Serve the Faraday middleware API.
10
+ #
11
+ # @param env [Hash{Symbol => Mixed}] the request
12
+ def call(env)
13
+ @app.call(env).on_complete do |res|
14
+ body = res[:body]
15
+
16
+ # Skip string bodies, they are unparsed or contain binary data
17
+ next if body.is_a? String
18
+
19
+ # Skip non-hash bodies, we cannot handle them here
20
+ next unless body.is_a? Hash
21
+
22
+ # Perform the key transformation
23
+ res[:body] = body.deep_underscore_keys
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PriceHubble
4
+ module Client
5
+ module Response
6
+ # Convert every response body to an +RecursiveOpenStruct+ for an easy
7
+ # access.
8
+ class RecursiveOpenStruct < Faraday::Middleware
9
+ # Serve the Faraday middleware API.
10
+ #
11
+ # @param env [Hash{Symbol => Mixed}] the request
12
+ def call(env)
13
+ @app.call(env).on_complete do |res|
14
+ body = res[:body]
15
+
16
+ # Skip string bodies, they are unparsed or contain binary data
17
+ next if body.is_a? String
18
+
19
+ # By definition empty responses (HTTP status 204)
20
+ # or actual empty bodies should be an empty hash
21
+ body = {} if res[:status] == 204 || res[:body].empty?
22
+
23
+ # Looks like we have some actual data we can wrap
24
+ res[:body] = \
25
+ ::RecursiveOpenStruct.new(body, recurse_over_arrays: true)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PriceHubble
4
+ module Client
5
+ module Utils
6
+ # Some helpers to prepare requests in a general way.
7
+ #
8
+ module Request
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ # A common HTTP content type to symbol
13
+ # mapping for correct header settings.
14
+ CONTENT_TYPE = {
15
+ json: 'application/json',
16
+ multipart: 'multipart/form-data',
17
+ url_encoded: 'application/x-www-form-urlencoded'
18
+ }.freeze
19
+
20
+ # Use the configured identity to authenticate the given request.
21
+ #
22
+ # @param req [Faraday::Request] the request to manipulate
23
+ def use_authentication(req)
24
+ req.params.merge!(access_token: PriceHubble.identity.access_token)
25
+ end
26
+
27
+ # Use the default request context to identificate the request.
28
+ #
29
+ # @param action [String, Symbol] the used client action
30
+ # @param req [Faraday::Request] the request to manipulate
31
+ def use_default_context(req, action)
32
+ req.options.context ||= {}
33
+ req.options.context.merge!(client: self.class,
34
+ action: action,
35
+ request_id: SecureRandom.hex(3))
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PriceHubble
4
+ module Client
5
+ module Utils
6
+ # Some helpers to work with responses in a general way.
7
+ module Response
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ # Simple helper to query the response status.
12
+ #
13
+ # @param res [Faraday::response] the response object
14
+ # @param code [Range, Array, Integer] the range of good
15
+ # response codes
16
+ # @return [Boolean] whenever the request got an allowed status
17
+ def status?(res, code: 0..399)
18
+ code = [code] unless code.is_a? Range
19
+ code = code.flatten if code.is_a? Array
20
+ code.include? res.status
21
+ end
22
+ alias_method :successful?, :status?
23
+
24
+ # A simple syntactic sugar helper to query the response status.
25
+ #
26
+ # @param res [Faraday::response] the response object
27
+ # @param code [Range] the range of failed response codes
28
+ # @return [Boolean] whenever the request failed
29
+ def failed?(res, code: 400..600)
30
+ status?(res, code: code)
31
+ end
32
+
33
+ # Perform a common error handling for entity responses. This allows a
34
+ # clean usage of the decision flow control. Here comes an example:
35
+ #
36
+ # decision do |result|
37
+ # result.bang(&bang_entity(entity, res, id: entity.id))
38
+ # end
39
+ #
40
+ # @param entity [PriceHubble::BaseEntity, Class] the result entity
41
+ # @param res [Faraday::Response] the response object
42
+ # @param data [Hash{Mixed => Mixed}] the request data
43
+ # @return [Proc] the proc which performs the error handling
44
+ def bang_entity(entity, res, data)
45
+ class_name = entity
46
+ class_name = entity.class unless entity.is_a? Class
47
+ lambda do
48
+ next PriceHubble::EntityNotFound.new(nil, class_name, data) \
49
+ if res.status == 404
50
+ next PriceHubble::EntityInvalid.new(res.body.message, entity) \
51
+ if res.status == 400
52
+
53
+ PriceHubble::RequestError.new(nil, res)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PriceHubble
4
+ module Client
5
+ # A high level client library for the PriceHubble Valuation API.
6
+ class Valuation < Base
7
+ # Perform a full-fledged valuation request.
8
+ #
9
+ # @param request [PriceHubble::ValuationRequest] the valuation request
10
+ # @param args [Hash{Symbol => Mixed}] the authentication credentials
11
+ # @return [Array<PriceHubble::Valuation>, nil] the valuation results,
12
+ # or +nil+ on error
13
+ #
14
+ # rubocop:disable Metrics/MethodLength because of the request handling
15
+ def property_value(request, **args)
16
+ data = request.attributes(true)
17
+ res = connection.post do |req|
18
+ req.path = '/api/v1/valuation/property_value'
19
+ req.body = data
20
+ use_default_context(req, :property_value)
21
+ use_authentication(req)
22
+ end
23
+ decision(bang: args.fetch(:bang, false)) do |result|
24
+ result.bang(&bang_entity(request, res, data))
25
+ result.good(&assign_valuations(res.body.valuations, request))
26
+ successful?(res)
27
+ end
28
+ end
29
+ # rubocop:enable Metrics/MethodLength
30
+
31
+ # Map and assign the valuation response to our local
32
+ # +PriceHubble::Valuation+ representation. While taking care of the
33
+ # multi-dimensional array structure which is reflected from the request
34
+ # data. You get back a lambda which you need to call to get the results
35
+ # (for use in a decision). The return values is a
36
+ # +Array<PriceHubble::Valuation>+.
37
+ #
38
+ # @param data [Array<Array<RecursiveOpenStruct>>] the raw response
39
+ # valuations data
40
+ # @param request [PriceHubble::ValuationRequest] the original request
41
+ # @return [Proc] the valuation mapping code
42
+ #
43
+ # rubocop:disable Metrics/MethodLength because of the request
44
+ # to response mapping
45
+ def assign_valuations(data, request)
46
+ lambda do
47
+ # valuations[i][j] contains the valuation for property i on date j
48
+ data.each_with_index.map do |valuations, property_idx|
49
+ valuations.each_with_index.map do |valuation, date_idx|
50
+ # Fetch the request data for this valuation and
51
+ # extend the raw data to build a local representation
52
+ valuation.property = request.properties[property_idx]
53
+ valuation.valuation_date = request.valuation_dates[date_idx]
54
+ valuation.deal_type = request.deal_type
55
+ valuation.country_code = request.country_code
56
+ # Build the local representation from the raw data
57
+ PriceHubble::Valuation.new(valuation)
58
+ end
59
+ end.flatten
60
+ end
61
+ end
62
+ # rubocop:enable Metrics/MethodLength
63
+
64
+ # Generate bang method variants
65
+ bangers :property_value
66
+ end
67
+ end
68
+ end