barkibu-kb 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/.env.example +3 -0
  3. data/.gitignore +20 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +45 -0
  6. data/.ruby-version +2 -0
  7. data/.travis.yml +7 -0
  8. data/CHANGELOG.md +185 -0
  9. data/CODE_OF_CONDUCT.md +74 -0
  10. data/Gemfile +7 -0
  11. data/Gemfile.lock +182 -0
  12. data/LICENSE.txt +21 -0
  13. data/README.md +243 -0
  14. data/Rakefile +28 -0
  15. data/barkibu-kb-fake.gemspec +41 -0
  16. data/barkibu-kb.gemspec +54 -0
  17. data/bin/console +14 -0
  18. data/bin/setup +8 -0
  19. data/docker-compose.yaml +15 -0
  20. data/lib/barkibu-kb-fake.rb +1 -0
  21. data/lib/barkibu-kb.rb +33 -0
  22. data/lib/kb/cache.rb +23 -0
  23. data/lib/kb/client.rb +85 -0
  24. data/lib/kb/client_resolver.rb +62 -0
  25. data/lib/kb/concerns/as_kb_wrapper.rb +67 -0
  26. data/lib/kb/concerns.rb +1 -0
  27. data/lib/kb/errors/client_error.rb +9 -0
  28. data/lib/kb/errors/conflict_error.rb +3 -0
  29. data/lib/kb/errors/error.rb +26 -0
  30. data/lib/kb/errors/resource_not_found.rb +3 -0
  31. data/lib/kb/errors/unprocessable_entity_error.rb +3 -0
  32. data/lib/kb/errors.rb +6 -0
  33. data/lib/kb/fake/api.rb +72 -0
  34. data/lib/kb/fake/bounded_context/pet_family/breeds.rb +15 -0
  35. data/lib/kb/fake/bounded_context/pet_family/hubspot_relationship.rb +17 -0
  36. data/lib/kb/fake/bounded_context/pet_family/pet_contracts.rb +24 -0
  37. data/lib/kb/fake/bounded_context/pet_family/pet_parents.rb +98 -0
  38. data/lib/kb/fake/bounded_context/pet_family/pets.rb +84 -0
  39. data/lib/kb/fake/bounded_context/pet_family/products.rb +28 -0
  40. data/lib/kb/fake/bounded_context/rest_resource.rb +134 -0
  41. data/lib/kb/fake.rb +6 -0
  42. data/lib/kb/inflections.rb +3 -0
  43. data/lib/kb/models/assessment.rb +58 -0
  44. data/lib/kb/models/base_model.rb +40 -0
  45. data/lib/kb/models/breed.rb +39 -0
  46. data/lib/kb/models/concerns/creatable.rb +18 -0
  47. data/lib/kb/models/concerns/destroyable.rb +17 -0
  48. data/lib/kb/models/concerns/find_or_creatable.rb +19 -0
  49. data/lib/kb/models/concerns/findable.rb +19 -0
  50. data/lib/kb/models/concerns/inspectionable.rb +13 -0
  51. data/lib/kb/models/concerns/listable.rb +21 -0
  52. data/lib/kb/models/concerns/queryable.rb +34 -0
  53. data/lib/kb/models/concerns/updatable.rb +18 -0
  54. data/lib/kb/models/concerns/upsertable.rb +17 -0
  55. data/lib/kb/models/concerns.rb +10 -0
  56. data/lib/kb/models/condition.rb +32 -0
  57. data/lib/kb/models/hubspot_relationship.rb +34 -0
  58. data/lib/kb/models/pet.rb +68 -0
  59. data/lib/kb/models/pet_contract.rb +77 -0
  60. data/lib/kb/models/pet_parent.rb +111 -0
  61. data/lib/kb/models/plan.rb +44 -0
  62. data/lib/kb/models/product.rb +34 -0
  63. data/lib/kb/models/symptom.rb +25 -0
  64. data/lib/kb/models.rb +15 -0
  65. data/lib/kb/type/array_of_conditions_type.rb +13 -0
  66. data/lib/kb/type/array_of_strings_type.rb +9 -0
  67. data/lib/kb/type/array_of_symptoms_type.rb +13 -0
  68. data/lib/kb/types.rb +7 -0
  69. data/lib/kb/validators/uniqueness_validator.rb +26 -0
  70. data/lib/kb/validators.rb +1 -0
  71. data/lib/kb/version.rb +3 -0
  72. metadata +325 -0
@@ -0,0 +1,9 @@
1
+ module KB
2
+ class ClientError < Error
3
+ def message
4
+ JSON.parse(body)['message']
5
+ rescue StandardError
6
+ body
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module KB
2
+ class ConflictError < ClientError; end
3
+ end
@@ -0,0 +1,26 @@
1
+ module KB
2
+ class Error < StandardError
3
+ attr_accessor :status_code, :body, :message
4
+
5
+ def initialize(status_code = nil, body = nil, error = nil)
6
+ super(error)
7
+ @status_code = status_code
8
+ @body = body
9
+ @message = "Received Status: #{status_code}\n#{body}"
10
+ set_backtrace error.backtrace if error
11
+ end
12
+
13
+ def self.from_faraday(error)
14
+ case error.response[:status]
15
+ when 404
16
+ ResourceNotFound
17
+ when 409
18
+ ConflictError
19
+ when 422
20
+ UnprocessableEntityError
21
+ else
22
+ self
23
+ end.new(error.response[:status], error.response[:body], error)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,3 @@
1
+ module KB
2
+ class ResourceNotFound < Error; end
3
+ end
@@ -0,0 +1,3 @@
1
+ module KB
2
+ class UnprocessableEntityError < ClientError; end
3
+ end
data/lib/kb/errors.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'kb/errors/error'
2
+ require 'kb/errors/client_error'
3
+
4
+ require 'kb/errors/conflict_error'
5
+ require 'kb/errors/unprocessable_entity_error'
6
+ require 'kb/errors/resource_not_found'
@@ -0,0 +1,72 @@
1
+ require 'kb/fake/bounded_context/pet_family/breeds'
2
+ require 'kb/fake/bounded_context/pet_family/pet_parents'
3
+ require 'kb/fake/bounded_context/pet_family/pets'
4
+ require 'kb/fake/bounded_context/pet_family/products'
5
+ require 'kb/fake/bounded_context/pet_family/pet_contracts'
6
+ require 'kb/fake/bounded_context/pet_family/hubspot_relationship'
7
+
8
+ module KB
9
+ module Fake
10
+ class ApiState
11
+ attr_accessor :petparents, :pets, :consultations, :petcontracts, :plans, :breeds, :products, :hubspot_relationship
12
+
13
+ # rubocop:disable Metrics/ParameterLists
14
+ def initialize(petparents: [], pets: [], consultations: [], petcontracts: [], plans: [], breeds: [],
15
+ products: [], hubspot_relationship: [])
16
+ @petparents = petparents
17
+ @pets = pets
18
+ @consultations = consultations
19
+ @petcontracts = petcontracts
20
+ @plans = plans
21
+ @breeds = breeds
22
+ @products = products
23
+ @hubspot_relationship = hubspot_relationship
24
+ end
25
+ # rubocop:enable Metrics/ParameterLists
26
+
27
+ def to_snapshot
28
+ {
29
+ pets: @pets.clone,
30
+ petparents: @petparents.clone,
31
+ consultations: @consultations.clone,
32
+ petcontracts: @petcontracts.clone,
33
+ plans: @plans.clone,
34
+ breeds: @breeds.clone,
35
+ products: @products.clone,
36
+ hubspot_relationship: @hubspot_relationship.clone
37
+ }
38
+ end
39
+ end
40
+
41
+ class Api < Sinatra::Base
42
+ include BoundedContext::PetFamily::Breeds
43
+ include BoundedContext::PetFamily::Pets
44
+ include BoundedContext::PetFamily::PetParents
45
+ include BoundedContext::PetFamily::PetContracts
46
+ include BoundedContext::PetFamily::Products
47
+ include BoundedContext::PetFamily::HubspotRelationship
48
+
49
+ set :state, ApiState.new
50
+
51
+ def self.snapshot
52
+ Api.state.to_snapshot
53
+ end
54
+
55
+ def self.restore(snapshot)
56
+ set :state, ApiState.new(**snapshot)
57
+ end
58
+
59
+ def resource_state(name)
60
+ Api.state.send(name)
61
+ end
62
+
63
+ def set_resource_state(name, value)
64
+ Api.state.send("#{name}=", value)
65
+ end
66
+
67
+ resource :consultations, except: %i[create update destroy]
68
+
69
+ resource :plans, except: %i[show create update destroy]
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,15 @@
1
+ require 'kb/fake/bounded_context/rest_resource'
2
+
3
+ module BoundedContext
4
+ module PetFamily
5
+ module Breeds
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ include RestResource
10
+
11
+ listen_on_index :breeds, :v1
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ require 'kb/fake/bounded_context/rest_resource'
2
+
3
+ module BoundedContext
4
+ module PetFamily
5
+ module HubspotRelationship
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ include RestResource
10
+
11
+ get '/v1/hubspot/:model/:key/relationship' do
12
+ resource_by_key(:hubspot_relationship, params['key'])
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,24 @@
1
+ require 'kb/fake/bounded_context/rest_resource'
2
+
3
+ module BoundedContext
4
+ module PetFamily
5
+ module PetContracts
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ include RestResource
10
+
11
+ get '/v1/petcontracts/contractnumber/:contract_number' do
12
+ resource = resource_state(:petcontracts).detect do |contract|
13
+ contract['contractNumber'] == params['contract_number']
14
+ end
15
+ return json_response 404, {} if resource.nil?
16
+
17
+ json_response 200, resource
18
+ end
19
+
20
+ resource :petcontracts, except: %i[index destroy]
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,98 @@
1
+ require 'kb/fake/bounded_context/rest_resource'
2
+ # rubocop:disable Metrics/BlockLength
3
+
4
+ module BoundedContext
5
+ module PetFamily
6
+ module PetParents
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ include RestResource
11
+
12
+ resource :petparents
13
+
14
+ def petparents_filterable_attributes
15
+ KB::PetParent::FIELDS.map { |k| k.to_s.camelize(:lower) }
16
+ end
17
+
18
+ get '/v1/petparents/:key/pets' do
19
+ json_response 200, pets_by_pet_parent_key(params['key'])
20
+ end
21
+
22
+ get '/v1/petparents/:key/contracts' do
23
+ pet_keys = pets_by_pet_parent_key(params['key']).map { |pet| pet['key'] }
24
+ contracts = resource_state(:petcontracts).select { |contract| pet_keys.include? contract['petKey'] }
25
+
26
+ json_response 200, contracts
27
+ end
28
+
29
+ put '/v1/petparents' do
30
+ params = JSON.parse(request.body.read)
31
+ existing_pet_parent = pet_parent_by_key(params) || pet_parent_by_email(params) || pet_parent_by_phone(params)
32
+ resource = (existing_pet_parent || { 'key' => SecureRandom.uuid }).merge params
33
+
34
+ if existing_pet_parent.present?
35
+ if same_phone_number_but_different_email?(existing_pet_parent, params)
36
+ return json_response 422, { error: 'Unprocessable Entity', message: 'Email can not be overridden' }
37
+ end
38
+
39
+ if same_email_but_different_phone_number?(existing_pet_parent, params)
40
+ previous_pet_parent_by_phone = pet_parent_by_phone(params)
41
+ if previous_pet_parent_by_phone.present?
42
+ return json_response 409,
43
+ { error: 'ConflictError',
44
+ message: 'Duplicated pet parent: same partner, phoneNumber \
45
+ and phoneNumberPrefix' }
46
+ end
47
+ end
48
+
49
+ update_resource_state(:petparents, resource)
50
+ else
51
+ resource_state(:petparents) << resource
52
+ end
53
+
54
+ json_response 200, resource
55
+ end
56
+
57
+ private
58
+
59
+ def pet_parent_by_key(params)
60
+ find_resource(:petparents, params['key']) if params['key']
61
+ end
62
+
63
+ def pet_parent_by_phone(params)
64
+ matches_by_phone = (if params['phoneNumber']
65
+ filter_resources(:petparents,
66
+ params.slice('phoneNumber',
67
+ 'prefixPhoneNumber'))
68
+ end)
69
+ matches_by_phone.first if matches_by_phone&.count == 1
70
+ end
71
+
72
+ def pet_parent_by_email(params)
73
+ matches_by_email = (filter_resources(:petparents, params.slice('email')) if params['email'])
74
+ matches_by_email.first if matches_by_email&.count == 1
75
+ end
76
+
77
+ def pets_by_pet_parent_key(key)
78
+ resource_state(:pets).select { |pet| pet['petParentKey'] == key }
79
+ end
80
+
81
+ def same_email_but_different_phone_number?(previous, new)
82
+ (previous['email'] == new['email']) &&
83
+ ((previous['phoneNumber'] != new['phoneNumber']) ||
84
+ (previous['prefixPhoneNumber'] != new['prefixPhoneNumber']))
85
+ end
86
+
87
+ def same_phone_number_but_different_email?(previous, new_resource)
88
+ return false unless new_resource.key?('email')
89
+
90
+ (previous['phoneNumber'] == new_resource['phoneNumber']) &&
91
+ (previous['prefixPhoneNumber'] == new_resource['prefixPhoneNumber']) &&
92
+ (previous['email'] != new_resource['email'])
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ # rubocop:enable Metrics/BlockLength
@@ -0,0 +1,84 @@
1
+ require 'kb/fake/bounded_context/rest_resource'
2
+ require 'date'
3
+ # rubocop:disable Metrics/BlockLength
4
+
5
+ module BoundedContext
6
+ module PetFamily
7
+ module Pets
8
+ extend ActiveSupport::Concern
9
+ include RestResource
10
+
11
+ included do
12
+ include RestResource
13
+
14
+ resource :pets
15
+
16
+ def pets_filterable_attributes
17
+ KB::Pet::FIELDS.map { |k| k.to_s.camelize(:lower) }
18
+ end
19
+
20
+ def on_pets_create(_version)
21
+ resource = JSON.parse(request.body.read)
22
+ resource['ageCategory'] = stage(resource['birthDate'], resource['species'])
23
+ resource = resource.merge 'key' => SecureRandom.uuid
24
+ resource_state(:pets) << resource
25
+ json_response 201, resource
26
+ end
27
+
28
+ def on_pets_update(_version)
29
+ resource_to_update = find_resource :pets, params['key']
30
+
31
+ return json_response 404, {} if resource_to_update.nil?
32
+
33
+ partial_resource = JSON.parse(request.body.read)
34
+ partial_resource['ageCategory'] = stage(partial_resource['birthDate'], resource_to_update['species'])
35
+ updated_resource = resource_to_update.merge partial_resource
36
+
37
+ update_resource_state(:pets, updated_resource)
38
+
39
+ json_response 200, updated_resource
40
+ end
41
+
42
+ get '/v1/pets/:key/contracts' do
43
+ contracts = resource_state(:petcontracts).select { |contract| contract['petKey'] == params['key'] }
44
+
45
+ json_response 200, contracts
46
+ end
47
+
48
+ put '/v1/pets' do
49
+ params = JSON.parse(request.body.read)
50
+ pet_parent = find_resource(:petparents, params['petParentKey'])
51
+
52
+ return json_response 422, {} if pet_parent.nil?
53
+
54
+ potential_matches = filter_resources(:pets, params.slice('name', 'petParentKey'))
55
+ existing_pet = (potential_matches.first if potential_matches.count == 1)
56
+
57
+ resource = (existing_pet || { 'key' => SecureRandom.uuid }).merge params
58
+
59
+ if existing_pet.present?
60
+ update_resource_state(:pets, resource)
61
+ else
62
+ resource_state(:pets) << resource
63
+ end
64
+
65
+ json_response 200, resource
66
+ end
67
+ end
68
+
69
+ def stage(birthdate, species)
70
+ return nil if birthdate.nil?
71
+
72
+ case ((Time.zone.now - Time.zone.parse(birthdate)) / 1.month).to_i
73
+ when 0..11
74
+ species == 'cat' ? 'kitten' : 'puppy'
75
+ when 12..99
76
+ 'adult'
77
+ else
78
+ 'senior'
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ # rubocop:enable Metrics/BlockLength
@@ -0,0 +1,28 @@
1
+ require 'kb/fake/bounded_context/rest_resource'
2
+
3
+ module BoundedContext
4
+ module PetFamily
5
+ module Products
6
+ extend ActiveSupport::Concern
7
+ include RestResource
8
+
9
+ included do
10
+ include RestResource
11
+
12
+ resource :products, except: %i[create update destroy]
13
+
14
+ def products_filterable_attributes
15
+ [:country]
16
+ end
17
+
18
+ def on_products_index(_version)
19
+ return json_response 400, {} if params['country'].nil?
20
+
21
+ return json_response 422, {} if ISO3166::Country.search(params['country']).nil?
22
+
23
+ json_response 200, filter_resources(:products, params)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,134 @@
1
+ # rubocop:disable Metrics/BlockLength
2
+ module BoundedContext
3
+ module RestResource
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ def resource_by_key(resource, key)
8
+ entity = find_resource(resource, key)
9
+ return json_response 404, {} if entity.nil?
10
+
11
+ json_response 200, entity
12
+ end
13
+
14
+ def json_response(response_code, body_content)
15
+ content_type :json
16
+ status response_code
17
+ body body_content.to_json
18
+ end
19
+
20
+ def on_index_action(name, version)
21
+ return send("on_#{name}_index", version) if respond_to? "on_#{name}_index"
22
+
23
+ json_response 200, filter_resources(name, params)
24
+ end
25
+
26
+ def filter_resources(name, filters)
27
+ resource_state(name).select do |item|
28
+ item[:deleted_at].blank? && filters.slice(*filterable_attributes(name)).reduce(true) do |sum, (key, value)|
29
+ sum && (value.blank? \
30
+ || (item.fetch(key, '') || '').downcase.include?(value.downcase))
31
+ end
32
+ end
33
+ end
34
+
35
+ def filterable_attributes(name)
36
+ try("#{name}_filterable_attributes") || []
37
+ end
38
+
39
+ def on_show_action(name, version)
40
+ return send("on_#{name}_show", version) if respond_to? "on_#{name}_show"
41
+
42
+ resource_by_key name, params['key']
43
+ end
44
+
45
+ def on_create_action(name, version)
46
+ return send("on_#{name}_create", version) if respond_to? "on_#{name}_create"
47
+
48
+ resource = JSON.parse(request.body.read)
49
+ resource = resource.merge 'key' => SecureRandom.uuid
50
+ resource_state(name) << resource
51
+ json_response 201, resource
52
+ end
53
+
54
+ def on_update_action(name, version)
55
+ return send("on_#{name}_update", version) if respond_to? "on_#{name}_update"
56
+
57
+ resource_to_update = find_resource name, params['key']
58
+
59
+ return json_response 404, {} if resource_to_update.nil?
60
+
61
+ partial_resource = JSON.parse(request.body.read)
62
+ updated_resource = resource_to_update.merge partial_resource
63
+
64
+ update_resource_state(name, updated_resource)
65
+
66
+ json_response 200, updated_resource
67
+ end
68
+
69
+ def on_destroy_action(name, version)
70
+ return send("on_#{name}_destroy", version) if respond_to? "on_#{name}_destroy"
71
+
72
+ resource_to_delete = find_resource name, params['key']
73
+ resource_to_delete[:deleted_at] = DateTime.now
74
+
75
+ update_resource_state(name, resource_to_delete)
76
+
77
+ json_response 204, nil
78
+ end
79
+
80
+ private
81
+
82
+ def find_resource(name, key)
83
+ resource_state(name).detect { |resource| resource['key'] == key }
84
+ end
85
+
86
+ def update_resource_state(name, updated_resource)
87
+ updated_resources = resource_state(name).map do |resource|
88
+ resource['key'] == updated_resource['key'] ? updated_resource : resource
89
+ end
90
+
91
+ set_resource_state(name, updated_resources)
92
+ end
93
+ end
94
+
95
+ class_methods do
96
+ def listen_on_index(name, version)
97
+ get "/#{version}/#{name}" do
98
+ on_index_action(name, version)
99
+ end
100
+ end
101
+
102
+ def listen_on_show(name, version)
103
+ get "/#{version}/#{name}/:key" do
104
+ on_show_action(name, version)
105
+ end
106
+ end
107
+
108
+ def listen_on_create(name, version)
109
+ post "/#{version}/#{name}" do
110
+ on_create_action(name, version)
111
+ end
112
+ end
113
+
114
+ def listen_on_update(name, version)
115
+ patch "/#{version}/#{name}/:key" do
116
+ on_update_action(name, version)
117
+ end
118
+ end
119
+
120
+ def listen_on_destroy(name, version)
121
+ delete "/#{version}/#{name}/:key" do
122
+ on_destroy_action(name, version)
123
+ end
124
+ end
125
+
126
+ def resource(name, version: 'v1', except: [])
127
+ %i[index show create update destroy].each do |action|
128
+ send("listen_on_#{action}", name, version) unless except.include?(action)
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ # rubocop:enable Metrics/BlockLength
data/lib/kb/fake.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'active_support'
2
+ require 'countries'
3
+ require 'sinatra'
4
+ require 'webmock'
5
+
6
+ require 'kb/fake/api'
@@ -0,0 +1,3 @@
1
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
2
+ inflect.acronym 'KB'
3
+ end
@@ -0,0 +1,58 @@
1
+ require 'kb/types'
2
+
3
+ module KB
4
+ class Assessment < BaseModel
5
+ include Findable
6
+ include Listable
7
+
8
+ kb_api :consultation
9
+
10
+ class << self
11
+ def by_pet(pet)
12
+ all(user: pet.kb_key)
13
+ end
14
+
15
+ def all(filters = {})
16
+ filters[:locale] ||= I18n.locale
17
+ filters[:pet_key] = filters[:user] if filters[:user].present?
18
+ super(filters)
19
+ end
20
+
21
+ def find(key, params = {})
22
+ params[:locale] ||= I18n.locale
23
+ super(key, params)
24
+ end
25
+
26
+ private
27
+
28
+ def attributes_from_response(response)
29
+ response.transform_keys(&:underscore).transform_keys(&:to_sym).slice(*FIELDS)
30
+ end
31
+ end
32
+
33
+ # Legacy Field Name From Anamnesis
34
+ alias_attribute :consultation_id, :key
35
+ alias_attribute :should_stop, :finished
36
+ alias_attribute :created_at, :date
37
+
38
+ attribute :invalid_symptoms, default: [] # Deprecated ?
39
+ attribute :conditions, :array_of_conditions
40
+ attribute :symptoms, :array_of_symptoms
41
+
42
+ attribute :date, :datetime
43
+ attribute :finished, :boolean, default: false
44
+
45
+ attribute :urgency, :string
46
+ attribute :key, :string
47
+ attribute :pet_key, :string
48
+
49
+ STRING_FIELDS = %i[key pet_key urgency].freeze
50
+ FIELDS = [*STRING_FIELDS, :date, :should_stop, :finished, :conditions, :symptoms, :next_question].freeze
51
+
52
+ def urgent
53
+ return false if urgency == 'low'
54
+
55
+ true
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,40 @@
1
+ module KB
2
+ class BaseModel
3
+ include Inspectionable
4
+ include ActiveModel::Model
5
+ include ActiveModel::Attributes
6
+ include ActiveModel::Serializers::JSON
7
+ include ActiveModel::Dirty
8
+
9
+ attr_accessor :persisted
10
+
11
+ define_model_callbacks :save
12
+ after_save :persist!
13
+
14
+ def initialize(attributes = {})
15
+ super
16
+ @persisted = false
17
+ yield self if block_given?
18
+ end
19
+
20
+ def persisted?
21
+ @persisted
22
+ end
23
+
24
+ def persist!
25
+ changes_applied
26
+ @persisted = true
27
+ end
28
+
29
+ def self.define_attribute_methods(*fields)
30
+ super
31
+ fields.each do |field|
32
+ define_method :"#{field}=" do |value|
33
+ super(value).tap do
34
+ public_send "#{field}_will_change!" if public_send("#{field}_changed?")
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,39 @@
1
+ module KB
2
+ class Breed < BaseModel
3
+ include Listable
4
+
5
+ DEFAULT_LOCALE = ENV.fetch('KB_BREEDS_DEFAULT_LOCALE', 'es-es')
6
+
7
+ kb_api :breed
8
+
9
+ def self.all(filters = {})
10
+ filters[:locale] ||= DEFAULT_LOCALE
11
+ super(filters)
12
+ end
13
+
14
+ def self.dogs(filters = {})
15
+ filters[:species] = 'dog'
16
+ all(filters)
17
+ end
18
+
19
+ def self.cats(filters = {})
20
+ filters[:species] = 'cat'
21
+ all(filters)
22
+ end
23
+
24
+ def self.attributes_from_response(response)
25
+ response.transform_keys(&:underscore).transform_keys(&:to_sym).slice(*FIELDS)
26
+ end
27
+
28
+ private_class_method :attributes_from_response
29
+
30
+ STRING_FIELDS = %i[key locale name species weight_group external_id].freeze
31
+ FIELDS = [*STRING_FIELDS].freeze
32
+
33
+ define_attribute_methods(*FIELDS)
34
+
35
+ STRING_FIELDS.each do |field|
36
+ attribute field, :string
37
+ end
38
+ end
39
+ end