barkibu-kb 0.16.1

Sign up to get free protection for your applications and to get access to all the features.
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