c80_shared 0.1.67 → 0.1.68

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/app/getboat/schemas/base.rb +67 -0
  3. data/app/getboat/schemas/base_boat_schema.rb +233 -0
  4. data/app/getboat/schemas/central_agent/save_boat_schema.rb +55 -0
  5. data/app/getboat/schemas/site/lease/create_personal_inquiry_schema.rb +187 -0
  6. data/app/repositories/accounts/central_agent_account_repository.rb +46 -0
  7. data/app/repositories/accounts/client_account_repository.rb +43 -0
  8. data/app/services/abstract_prices_service.rb +70 -0
  9. data/app/services/boats/boat_prices_save_service.rb +79 -0
  10. data/app/services/boats/boat_sale_prices_save_service.rb +58 -0
  11. data/app/services/central_agent/save_boat_service.rb +117 -0
  12. data/app/services/dimension_service.rb +42 -0
  13. data/app/services/lease/create_broadcast_inquiry_service.rb +68 -0
  14. data/config/initializers/core_ext/string.rb +62 -0
  15. data/lib/c80_shared/version.rb +1 -1
  16. data/lib/c80_shared.rb +27 -0
  17. data/lib/dry/errors.rb +77 -0
  18. data/lib/dry/rule.rb +69 -0
  19. data/lib/dry/rules/and.rb +11 -0
  20. data/lib/dry/rules/between.rb +18 -0
  21. data/lib/dry/rules/binary.rb +16 -0
  22. data/lib/dry/rules/collection.rb +16 -0
  23. data/lib/dry/rules/composite.rb +19 -0
  24. data/lib/dry/rules/equal.rb +18 -0
  25. data/lib/dry/rules/format.rb +18 -0
  26. data/lib/dry/rules/greater_than.rb +18 -0
  27. data/lib/dry/rules/greater_than_or_equal.rb +18 -0
  28. data/lib/dry/rules/included.rb +18 -0
  29. data/lib/dry/rules/length_between.rb +18 -0
  30. data/lib/dry/rules/length_equal.rb +18 -0
  31. data/lib/dry/rules/less_than.rb +18 -0
  32. data/lib/dry/rules/less_than_or_equal.rb +18 -0
  33. data/lib/dry/rules/max_length.rb +18 -0
  34. data/lib/dry/rules/min_length.rb +18 -0
  35. data/lib/dry/rules/not_equal.rb +18 -0
  36. data/lib/dry/rules/or.rb +13 -0
  37. data/lib/dry/rules/present.rb +21 -0
  38. data/lib/dry/rules/then.rb +13 -0
  39. data/lib/dry/rules_factory.rb +118 -0
  40. data/lib/dry/schema.rb +148 -0
  41. metadata +39 -2
@@ -0,0 +1,70 @@
1
+ class AbstractPricesService
2
+
3
+ attr_reader :built_prices
4
+
5
+ def initialize(all_currencies = ::Dicts::Currency::ALL2) # all_currencies - список валют, в которых надо сохранить цены
6
+ @all_currencies = all_currencies
7
+ _reset_ivars
8
+ end
9
+
10
+ private
11
+
12
+ def _model
13
+ raise NotImplementedError
14
+ end
15
+
16
+ def _fields_for_create
17
+ raise NotImplementedError
18
+ end
19
+
20
+ def _save(prices) # помещаем в базу всё одним запросом
21
+ filtered = prices.select { |el| !el[:_delete] && el[:value].to_i != 0 } # игнорируя цены, которые были помечены "к удалению" или те, у которых значение 0 (0 можно подавать, если хотим "удалить" цену продажи)
22
+ return if filtered.size.zero?
23
+
24
+ values = filtered.map do |p|
25
+ _fields_for_create.map do |field|
26
+ p[field.to_sym]
27
+ end.compact * ','
28
+ end * '),('
29
+ values = '(%s)' % values
30
+ sql = 'INSERT INTO %s(%s) VALUES %s' % [_model.table_name, _fields_for_create * ',', values]
31
+ ActiveRecord::Base.connection.execute sql
32
+
33
+ filtered
34
+ end
35
+
36
+ # из базы удаляем все записи, принадлежащие данной лодке
37
+ # т.к. считаем, что с формы приходит полная картина по ценам
38
+ # кроме того, юзер может просто изменить, например, период в уже
39
+ # существующей строке с ценами, которую он видит на экране
40
+ def _delete_existing_records(boat_id)
41
+ sql = 'DELETE FROM %s WHERE boat_id = %s' % [_model.table_name, boat_id]
42
+ ActiveRecord::Base.connection.execute sql
43
+ end
44
+
45
+ def _build_other(price) # отталкиваясь от валюты входных данных, рассчитываем "те же" цены в других валютах
46
+ return if price[:_delete] || price[:value].to_i == 0 # игнорируя цены, которые были помечены "к удалению" или те, у которых значение 0 (0 можно подавать, если хотим "удалить" цену продажи)
47
+
48
+ incoming_curr = ::Dicts::Currency.find(price[:currency_id])
49
+ currs_to_build = @all_currencies - [incoming_curr]
50
+ rates = Currency.fresh(incoming_curr.index).rates
51
+ rates = JSON.parse(rates)['rates']
52
+
53
+ currs_to_build.each do |curr|
54
+ rate = rates[curr.index]
55
+ res = price.clone
56
+ res[:currency_id] = curr.id
57
+ res[:value] = res[:value].to_f * rate
58
+ res[:is_orig] = false
59
+ @built_prices << res
60
+ end
61
+ end
62
+
63
+ def _delete_bad_values(prices)
64
+ prices.delete_if { |price| price[:value].nil? || price[:value].to_f.zero? }
65
+ end
66
+
67
+ def _reset_ivars
68
+ @built_prices = []
69
+ end
70
+ end
@@ -0,0 +1,79 @@
1
+ module Boats
2
+ #
3
+ # spec/services/boat/boat_prices_save_service_spec.rb
4
+ #
5
+ class BoatPricesSaveService < ::AbstractPricesService
6
+
7
+ attr_reader :is_for_rent
8
+
9
+ def self.perform(boat, params)
10
+ new.perform(boat, params)
11
+ end
12
+
13
+ def perform(boat, prices = nil)
14
+ _reset_ivars
15
+ return false if prices.nil?
16
+
17
+ boat_prices_params = prices.deep_dup # Rails deep_dup https://apidock.com/rails/Hash/deep_dup
18
+ boat_prices_params = boat_prices_params.map { |bp| bp.symbolize_keys }
19
+ filtered = _delete_bad_values boat_prices_params
20
+ return false if filtered.size.zero? # взаимодействия с базой не будет, если все цены с "плохими" значениями
21
+
22
+ filtered.each do |bp| # bp = {uom_id, season_id, currency_id, duration, value, discount}
23
+ bp[:boat_id] = boat.id
24
+ bp[:is_orig] = true
25
+ bp[:value] = bp[:value].is_a?(String) ? bp[:value].split(/[, ]/).join : bp[:value] # с формы может прийти строка, сгруппированная по разрадям, вида "100,100,222"
26
+ bp[:duration] = format('%.1f', bp[:duration])
27
+ bp[:discount] = bp[:discount].to_f # с формы может прийти пустая строка ''
28
+ bp[:created_at] = '\'%s\'' % Time.now.to_s(:db)
29
+ _build_other bp
30
+ end
31
+
32
+ filtered2 = _del_duplicates(filtered + @built_prices)
33
+ return false if filtered2.size.zero?
34
+
35
+ saved = nil
36
+ ActiveRecord::Base.transaction do
37
+ _delete_existing_records boat.id
38
+ saved = _save filtered2
39
+ end
40
+
41
+ @is_for_rent = _detect_for_rent saved
42
+ true
43
+ end
44
+
45
+ private
46
+
47
+ def _model
48
+ BoatPrice
49
+ end
50
+
51
+ def _fields_for_create
52
+ @fields_for_create ||= %w'boat_id uom_id duration season_id is_orig discount currency_id value created_at'
53
+ end
54
+
55
+
56
+ def _delete_bad_values(prices)
57
+ prices.delete_if do |price|
58
+ price[:value].nil? || price[:value].to_f.zero? || format('%.1f', price[:duration].to_f) == '0.0'
59
+ end
60
+ end
61
+
62
+ def _del_duplicates(params) # подстраховываемся, вдруг форма прислала что-то не то
63
+ grouped = params.group_by { |row| [row[:duration].to_f, row[:uom_id], row[:season_id], row[:currency_id]] } # убираем из params массива дубликаты согласно уникальному индексу в db
64
+ grouped.map {|_, v| v.first }
65
+ end
66
+
67
+ def _reset_ivars
68
+ @is_for_rent = nil
69
+ super
70
+ end
71
+
72
+ def _detect_for_rent(saved_prices)
73
+ return 0 if saved_prices.nil?
74
+ # noinspection RubySimplifyBooleanInspection
75
+ !!saved_prices.select { |bp| !bp[:_delete] }.size.nonzero? ? 1 : 0
76
+ end
77
+
78
+ end
79
+ end
@@ -0,0 +1,58 @@
1
+ module Boats
2
+ #
3
+ # [spec/services/boat/boat_sale_price_save_service_spec.rb]
4
+ #
5
+ class BoatSalePricesSaveService < ::AbstractPricesService
6
+
7
+ attr_reader :is_for_sale
8
+
9
+ def self.perform(boat_id, price)
10
+ new.perform boat_id, price
11
+ end
12
+
13
+ def perform(boat_id, price = nil)
14
+ _reset_ivars
15
+ return false if price.nil?
16
+
17
+ sp = price.dup
18
+ sp[:boat_id] = boat_id
19
+ sp[:is_orig] = true
20
+ sp[:value] = sp[:value].is_a?(String) ? sp[:value].split(/[, ]/).join : sp[:value].to_i # с формы может прийти строка, сгруппированная по разрадям, вида "100,100,222" + тут же превращаем nil в 0
21
+ sp[:discount] = sp[:discount].present? ? sp[:discount] : 0
22
+ sp[:created_at] = '\'%s\'' % Time.now.to_s(:db)
23
+ _build_other sp
24
+
25
+ filtered2 = @built_prices + [sp]
26
+ saved = nil
27
+ ActiveRecord::Base.transaction do
28
+ _delete_existing_records boat_id
29
+ saved = _save filtered2
30
+ end
31
+
32
+ @is_for_sale = _detect_for_sale saved
33
+ true
34
+ end
35
+
36
+ private
37
+
38
+ def _model
39
+ BoatSalePrice
40
+ end
41
+
42
+ def _fields_for_create
43
+ @fields_for_create ||= %w'boat_id currency_id value discount is_orig created_at'
44
+ end
45
+
46
+ def _reset_ivars
47
+ @is_for_sale = nil
48
+ super
49
+ end
50
+
51
+ def _detect_for_sale(saved_prices)
52
+ return 0 if saved_prices.nil?
53
+ # noinspection RubySimplifyBooleanInspection
54
+ !!saved_prices.select { |bp| bp[:value].to_i != 0 }.size.nonzero? ? 1 : 0
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,117 @@
1
+ module CentralAgent
2
+ class SaveBoatService
3
+
4
+ attr_reader :errors, :boat
5
+
6
+ def initialize
7
+ _reset_ivars
8
+ end
9
+
10
+ # @param [Hash] boat_params
11
+ # @param [Hash] cookies example: {... "currency"=>"USD", "dimension"=>"ft", "volume"=>"liters", "business"=>"rent", "language"=>"en"}
12
+ #
13
+ def perform(boat_params, cookies, current_user, state: Boat::STATE_PREMODERATED)
14
+ _reset_ivars
15
+
16
+ @schema = ::Schemas::CentralAgent::SaveBoatSchema.new boat_params.dup # проверяем параметры, пришедшие с формы
17
+ unless @schema.valid?
18
+ @errors = @schema.errors.messages
19
+ return false
20
+ end
21
+
22
+ if boat_params[:id].present?
23
+ boat = _update_boat(@schema.attributes, cookies)
24
+ else
25
+ boat = _create_boat(@schema.attributes, cookies, current_user, state)
26
+ end
27
+
28
+ @boat = boat
29
+ true
30
+ end
31
+
32
+ private
33
+
34
+ def _create_boat(boat_params, cookies, current_user, state) # TODO:: проверить и доработать при необходимости (ref /home/scout/git/get-the-boat/app/services/boat_register_service.rb)
35
+ prms = _fuck_params boat_params
36
+
37
+ boat = Boat.new
38
+ boat.assign_attributes prms
39
+ boat.state = state
40
+
41
+ _handle_boat_attributes(boat, cookies)
42
+
43
+ ActiveRecord::Base.transaction do
44
+ boat.users << current_user
45
+
46
+ boat.send :set_slug
47
+ boat.save validate: false
48
+ boat.update_attribute(:boat_photo_id, boat.boat_photos.first.id) # TODO:: реализовать назначение главного фото (пока же на скорую руку делаем первую фотку главной)
49
+ _save_prices boat
50
+ end
51
+
52
+ boat
53
+ end
54
+
55
+ def _update_boat(boat_params, cookies) # TODO:: реализовать назначение главного фото (в админке, например, приходит такое: "boat"=>{"boat_photo_id"=>"41434"})
56
+ # _fuck_params - подгоняем под active_record (перегоняем массив в хэш)
57
+ #boat_locs2 = boat_params[:boat_locations_attributes].compact.map { |attr| [attr[:id], attr] }.to_h
58
+ #prms = boat_params.except(:rent_prices, :sale_price, :boat_locations_attributes) # с формы из кабинета, в отличии от админки, приходит длина в виде "boat_length_metrics_ft" => "12.0"
59
+ #prms[:boat_locations_attributes] = boat_locs2
60
+ prms = _fuck_params boat_params
61
+
62
+ boat = Boat.find boat_params[:id]
63
+ boat.assign_attributes prms
64
+
65
+ _handle_boat_attributes(boat, cookies)
66
+
67
+ ActiveRecord::Base.transaction do
68
+ boat.send :set_slug
69
+ boat.save validate: false
70
+ _save_prices boat
71
+ boat.cancel if boat.state == ::Boat::STATE_APPROVED.to_s # кто знает, что там наизменял агент в своей лодке, вдруг фотки с телефонами загрузил?
72
+ end
73
+
74
+ boat
75
+ end
76
+
77
+ # boat_params - это атрибуты схемы
78
+ def _fuck_params(boat_params)
79
+ # подгоняем под active_record (перегоняем массив в хэш)
80
+ boat_locs2 = boat_params[:boat_locations_attributes].compact.map { |attr| [(attr[:id] || -(rand*1000).to_i), attr] }.to_h
81
+ prms = boat_params.except(:rent_prices, :sale_price, :boat_locations_attributes, :main_boat_photo_id) # с формы из кабинета, в отличии от админки, приходит длина в виде "boat_length_metrics_ft" => "12.0"
82
+ prms[:boat_photo_id] = boat_params[:main_boat_photo_id]
83
+ prms[:boat_locations_attributes] = boat_locs2
84
+ prms
85
+ end
86
+
87
+ def _handle_boat_attributes(boat, cookies)
88
+ dimension_service = ::DimensionService.new(boat) # присланные измерения переведём в другие единицы измерения
89
+ dimension_service.calculate_boat_attributes(cookies[:dimension])
90
+
91
+ # volume_service = VolumeService.new(boat)
92
+ # volume_service.calculate_boat_attributes(cookies[:volume])
93
+ end
94
+
95
+ # сохраним цены аренды и цену продажи
96
+ def _save_prices(boat)
97
+ service = ::Boats::BoatPricesSaveService.new
98
+ result = service.perform boat, @schema.attributes[:rent_prices]
99
+
100
+ if result && boat.for_rent != service.is_for_rent # result = true - обязательное условие
101
+ boat.update_columns for_rent: service.is_for_rent
102
+ end
103
+
104
+ service = ::Boats::BoatSalePricesSaveService.new
105
+ result = service.perform boat.id, @schema.attributes[:sale_price]
106
+
107
+ if result && boat.for_sale != service.is_for_sale # result = true - обязательное условие
108
+ boat.update_columns for_sale: service.is_for_sale
109
+ end
110
+ end
111
+
112
+ def _reset_ivars
113
+ @errors = { }
114
+ @boat = nil
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,42 @@
1
+ class DimensionService
2
+
3
+ def initialize model
4
+ @model = model
5
+ end
6
+
7
+ def calculate_boat_attributes default_dimension
8
+
9
+ attributes = [:boat_length_metrics, :boat_beam_metrics, :boat_draft_metrics, :boat_gross_tonage]
10
+
11
+ available_dimensions(default_dimension).each do |available_dimension|
12
+
13
+ # puts "#{available_dimension} #{default_dimension}"
14
+ dimension_value = Dimension::COMPARISON[default_dimension.to_sym][available_dimension.to_sym]
15
+
16
+ attributes.each do |attribute|
17
+ attribute_dimension = attribute.to_s + "_#{available_dimension}"
18
+ attribute_default_dimension = attribute.to_s + "_#{default_dimension}"
19
+ default_dimension_value = @model.attributes[attribute_default_dimension.downcase]
20
+
21
+ if default_dimension_value
22
+ calculated_dimension = default_dimension_value * dimension_value
23
+ @model.send("#{attribute_dimension}=", calculated_dimension)
24
+ # puts "#{default_dimension_value} #{default_dimension} is #{calculated_dimension} #{available_dimension}"
25
+ else
26
+ @model.send("#{attribute_dimension}=", nil)
27
+ end
28
+ end
29
+ end
30
+
31
+ end
32
+
33
+ def available_dimensions default_dimension
34
+ available_dimensions = []
35
+ I18n::t('static.dimensions').select do |dimension|
36
+ next if dimension[:id] == default_dimension
37
+ available_dimensions << dimension[:id]
38
+ end
39
+ available_dimensions
40
+ end
41
+
42
+ end
@@ -0,0 +1,68 @@
1
+ module Lease
2
+ #
3
+ # Если заявка создаётся НЕ со страницы лодки - используется этот сервис
4
+ #
5
+ class CreateBroadcastInquiryService
6
+
7
+ attr_reader :inquiry, :errors
8
+
9
+ def initialize
10
+ _reset_instance_variables
11
+ end
12
+
13
+ def perform(client_account, params)
14
+ _reset_instance_variables
15
+
16
+ @inquiry = ::Lease::Inquiry.new
17
+ _assign_attributes(client_account, params)
18
+ return false unless @inquiry.valid?
19
+
20
+ inquiry_profile = ::Lease::InquiryProfile.new inquiry: @inquiry
21
+
22
+ @inquiry.transaction do
23
+ @inquiry.save!
24
+ inquiry_profile.save!
25
+ end
26
+
27
+ if params[:uploaded_license_file].present?
28
+ ::Lease::LicenseService.new.process_license_file(client_account, params[:uploaded_license_file])
29
+ end
30
+
31
+ true
32
+ end
33
+
34
+ protected
35
+
36
+ def _assign_attributes(client_account, params)
37
+ @inquiry.client_account = client_account
38
+ @inquiry.address = params[:address]
39
+ @inquiry.latitude = params[:latitude]
40
+ @inquiry.longitude = params[:longitude]
41
+ @inquiry.from = params[:from]
42
+ @inquiry.to = params[:to]
43
+ @inquiry.name = params[:name]
44
+ @inquiry.phone_number = params[:phone_number]
45
+ @inquiry.is_skippered = params[:is_skippered]
46
+ @inquiry.comments = params[:comments]
47
+ @inquiry.need_transfer = params[:need_transfer]
48
+ @inquiry.watersports = params[:watersports]
49
+ @inquiry.guests = params[:guests]
50
+ @inquiry.kids = params[:kids]
51
+ @inquiry.boat_type_ids = params[:boat_types] # fuckin' rent_form.rb
52
+ @inquiry.staying_overnight = params[:staying_overnight]
53
+ @inquiry.hours_per_day = params[:hours_per_day]&.join(',')
54
+
55
+ if params[:my_price].present? && !params[:my_price].to_i.zero?
56
+ @inquiry.my_price = params[:my_price]
57
+ @inquiry.my_price_currency = params[:my_price_currency]
58
+ @inquiry.is_my_price = true
59
+ end
60
+ end
61
+
62
+ def _reset_instance_variables
63
+ @errors = { }
64
+ @inquiry = nil
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,62 @@
1
+ class String
2
+
3
+ def clear_query
4
+ dup.clear_query!
5
+ end
6
+
7
+ def clear_query!
8
+ gsub!(/\s+/, ' ')
9
+ strip!
10
+ self
11
+ end
12
+
13
+ def nums_only
14
+ scan(/\d/).join
15
+ end
16
+
17
+ def numeric?
18
+ true if Float(self) rescue false
19
+ end
20
+
21
+ def to_bool
22
+ %w[t true yes on 1].include? self
23
+ end
24
+
25
+ def sanitize(options = {})
26
+ ActionController::Base.helpers.sanitize(self, options)
27
+ end
28
+
29
+ def nl2br
30
+ dup.nl2br!
31
+ end
32
+
33
+ def nl2br!
34
+ gsub!(/\r/, '')
35
+ gsub!(/\n/, '<br>')
36
+ self
37
+ end
38
+
39
+ def strip_leading_zeros
40
+ dup.strip_leading_zeros!
41
+ end
42
+
43
+ def strip_leading_zeros!
44
+ gsub!(/^0+/, '')
45
+ self
46
+ end
47
+
48
+ def try_to_integer
49
+ Integer( try_to_big_decimal ) rescue self
50
+ end
51
+
52
+ def try_to_big_decimal
53
+ gsub!(/\s/, '')
54
+ begin
55
+ Float(self)
56
+ rescue
57
+ return self
58
+ end
59
+ BigDecimal(self)
60
+ end
61
+
62
+ end
@@ -1,3 +1,3 @@
1
1
  module C80Shared
2
- VERSION = "0.1.67"
2
+ VERSION = "0.1.68"
3
3
  end
data/lib/c80_shared.rb CHANGED
@@ -3,6 +3,33 @@ lambda do
3
3
  lib_path = '%s/**/*.rb' % File.expand_path('../c80_shared', __FILE__)
4
4
  # noinspection RubyResolve
5
5
  Dir[lib_path].each { |f| require_relative f }
6
+
7
+ require_relative 'dry/errors'
8
+ require_relative 'dry/schema'
9
+ require_relative 'dry/rule'
10
+
11
+ require_relative 'dry/rules/binary'
12
+ require_relative 'dry/rules/composite'
13
+ require_relative 'dry/rules/between'
14
+ require_relative 'dry/rules/min_length'
15
+ require_relative 'dry/rules/greater_than'
16
+ require_relative 'dry/rules/not_equal'
17
+ require_relative 'dry/rules/less_than'
18
+ require_relative 'dry/rules/greater_than_or_equal'
19
+ require_relative 'dry/rules/collection'
20
+ require_relative 'dry/rules/length_between'
21
+ require_relative 'dry/rules/or'
22
+ require_relative 'dry/rules/and'
23
+ require_relative 'dry/rules/less_than_or_equal'
24
+ require_relative 'dry/rules/then'
25
+ require_relative 'dry/rules/format'
26
+ require_relative 'dry/rules/present'
27
+ require_relative 'dry/rules/length_equal'
28
+ require_relative 'dry/rules/included'
29
+ require_relative 'dry/rules/equal'
30
+ require_relative 'dry/rules/max_length'
31
+
32
+ require_relative 'dry/rules_factory'
6
33
  end.call
7
34
 
8
35
  # загружаем все классы из директории app
data/lib/dry/errors.rb ADDED
@@ -0,0 +1,77 @@
1
+ module Dry
2
+ class Errors
3
+
4
+ attr_reader :messages
5
+
6
+ def initialize(messages = {})
7
+ @messages = messages
8
+ end
9
+
10
+
11
+ def add(key, message)
12
+ keys = key.to_s.split('.').map!(&:to_sym)
13
+ old = messages.dig(*keys[0...-1]) rescue {}
14
+ old = {} unless old.is_a?(Hash)
15
+ new = keys[0...-1].inject(messages) { |h, k| h[k] ||= {} rescue h = {}; h[k] = {} }
16
+ new[keys.last] = [] unless new.is_a?(Array)
17
+ new[keys.last] << message
18
+ new.merge!(old)
19
+ end
20
+
21
+
22
+ def merge!(error, parent_key = nil)
23
+ hash_to_dots(error.messages, {}, parent_key).each do |key, messages|
24
+ messages.each { |message| add(key, message) }
25
+ end
26
+ messages
27
+ end
28
+
29
+
30
+ def any?
31
+ messages.any?
32
+ end
33
+
34
+
35
+ def has_key?(key)
36
+ keys = key.to_s.split('.').map!(&:to_sym)
37
+ keys.size == 1 ? messages[keys.first].present? : (messages.dig(*keys[0...-1])[keys.last].present? rescue false)
38
+ end
39
+
40
+
41
+ def first_message
42
+ fetch_messages(messages.values.first).first
43
+ end
44
+
45
+
46
+ def clone
47
+ self.class.new(messages.clone)
48
+ end
49
+
50
+
51
+ private
52
+
53
+
54
+ def hash_to_dots(hash, results = {}, start_key = '')
55
+ hash.each do |key, value|
56
+ key = key.to_s
57
+ key_value = start_key.present? ? sprintf('%s.%s', start_key, key) : key
58
+ if value.is_a?(Hash)
59
+ results.merge!(hash_to_dots(value, results, key_value))
60
+ else
61
+ results[key_value] = value
62
+ end
63
+ end
64
+ results
65
+ end
66
+
67
+
68
+ def fetch_messages(value)
69
+ if value.is_a?(Hash)
70
+ fetch_messages(value.values.first)
71
+ else
72
+ value
73
+ end
74
+ end
75
+
76
+ end
77
+ end
data/lib/dry/rule.rb ADDED
@@ -0,0 +1,69 @@
1
+ module Dry
2
+ class Rule
3
+
4
+ attr_reader :value, :errors, :args
5
+
6
+ def initialize(value, errors = Dry::Errors.new, **args)
7
+ @value = value
8
+ @errors = errors
9
+ @args = args.symbolize_keys
10
+ end
11
+
12
+
13
+ def name
14
+ args[:name]
15
+ end
16
+
17
+
18
+ def add_error
19
+ errors.add(key, messages[name.to_s] || 'invalid')
20
+ end
21
+
22
+
23
+ def clone
24
+ self.class.new(value, errors.clone, args)
25
+ end
26
+
27
+
28
+ def and(right)
29
+ Dry::Rules::And.new(self, errors, args.merge(right: right))
30
+ end
31
+ alias :& :and
32
+
33
+
34
+ def then(right)
35
+ Dry::Rules::Then.new(self, errors, args.merge(right: right))
36
+ end
37
+ alias :> :then
38
+
39
+
40
+ def or(right)
41
+ Dry::Rules::Or.new(self, errors, args.merge(right: right))
42
+ end
43
+ alias :| :or
44
+
45
+
46
+ def +(right)
47
+ Dry::Rules::Collection.new(self, errors, args.merge(right: right))
48
+ end
49
+
50
+
51
+ def valid?
52
+ raise NotImplementedError
53
+ end
54
+
55
+
56
+ private
57
+
58
+
59
+ def messages
60
+ @messages ||= (args[:messages] || {}).deep_stringify_keys
61
+ end
62
+
63
+
64
+ def key
65
+ @key ||= args[:key] || (raise 'Missing required param "key"')
66
+ end
67
+
68
+ end
69
+ end