c80_shared 0.1.67 → 0.1.68

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