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,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