cmdx 0.1.0

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 (103) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/.rspec +4 -0
  4. data/.rubocop.yml +64 -0
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +5 -0
  7. data/CODE_OF_CONDUCT.md +132 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +76 -0
  10. data/Rakefile +12 -0
  11. data/docs/basics/call.md +31 -0
  12. data/docs/basics/context.md +67 -0
  13. data/docs/basics/run.md +34 -0
  14. data/docs/basics/setup.md +32 -0
  15. data/docs/batch.md +53 -0
  16. data/docs/configuration.md +57 -0
  17. data/docs/example.md +82 -0
  18. data/docs/getting_started.md +47 -0
  19. data/docs/hooks.md +59 -0
  20. data/docs/interruptions/exceptions.md +29 -0
  21. data/docs/interruptions/faults.md +89 -0
  22. data/docs/interruptions/halt.md +80 -0
  23. data/docs/logging.md +102 -0
  24. data/docs/outcomes/result.md +17 -0
  25. data/docs/outcomes/states.md +31 -0
  26. data/docs/outcomes/statuses.md +33 -0
  27. data/docs/parameters/coercions.md +52 -0
  28. data/docs/parameters/defaults.md +47 -0
  29. data/docs/parameters/definitions.md +123 -0
  30. data/docs/parameters/namespacing.md +57 -0
  31. data/docs/parameters/validations.md +312 -0
  32. data/docs/tips_and_tricks.md +79 -0
  33. data/lib/cmdx/.DS_Store +0 -0
  34. data/lib/cmdx/batch.rb +43 -0
  35. data/lib/cmdx/coercions/array.rb +15 -0
  36. data/lib/cmdx/coercions/big_decimal.rb +23 -0
  37. data/lib/cmdx/coercions/boolean.rb +27 -0
  38. data/lib/cmdx/coercions/complex.rb +21 -0
  39. data/lib/cmdx/coercions/date.rb +26 -0
  40. data/lib/cmdx/coercions/date_time.rb +26 -0
  41. data/lib/cmdx/coercions/float.rb +21 -0
  42. data/lib/cmdx/coercions/hash.rb +31 -0
  43. data/lib/cmdx/coercions/integer.rb +21 -0
  44. data/lib/cmdx/coercions/rational.rb +21 -0
  45. data/lib/cmdx/coercions/string.rb +15 -0
  46. data/lib/cmdx/coercions/time.rb +26 -0
  47. data/lib/cmdx/coercions/virtual.rb +15 -0
  48. data/lib/cmdx/configuration.rb +25 -0
  49. data/lib/cmdx/context.rb +15 -0
  50. data/lib/cmdx/core_ext/hash.rb +36 -0
  51. data/lib/cmdx/core_ext/module.rb +48 -0
  52. data/lib/cmdx/core_ext/object.rb +55 -0
  53. data/lib/cmdx/error.rb +23 -0
  54. data/lib/cmdx/errors.rb +92 -0
  55. data/lib/cmdx/fault.rb +47 -0
  56. data/lib/cmdx/faults.rb +11 -0
  57. data/lib/cmdx/immutator.rb +21 -0
  58. data/lib/cmdx/lazy_struct.rb +79 -0
  59. data/lib/cmdx/log_formatters/json.rb +13 -0
  60. data/lib/cmdx/log_formatters/key_value.rb +13 -0
  61. data/lib/cmdx/log_formatters/line.rb +14 -0
  62. data/lib/cmdx/log_formatters/logstash.rb +18 -0
  63. data/lib/cmdx/log_formatters/raw.rb +13 -0
  64. data/lib/cmdx/logger.rb +16 -0
  65. data/lib/cmdx/parameter.rb +101 -0
  66. data/lib/cmdx/parameter_inspector.rb +23 -0
  67. data/lib/cmdx/parameter_serializer.rb +20 -0
  68. data/lib/cmdx/parameter_validator.rb +19 -0
  69. data/lib/cmdx/parameter_value.rb +136 -0
  70. data/lib/cmdx/parameters.rb +34 -0
  71. data/lib/cmdx/parameters_inspector.rb +13 -0
  72. data/lib/cmdx/parameters_serializer.rb +13 -0
  73. data/lib/cmdx/railtie.rb +32 -0
  74. data/lib/cmdx/result.rb +170 -0
  75. data/lib/cmdx/result_inspector.rb +31 -0
  76. data/lib/cmdx/result_logger.rb +22 -0
  77. data/lib/cmdx/result_serializer.rb +38 -0
  78. data/lib/cmdx/run.rb +33 -0
  79. data/lib/cmdx/run_inspector.rb +21 -0
  80. data/lib/cmdx/run_serializer.rb +16 -0
  81. data/lib/cmdx/task.rb +151 -0
  82. data/lib/cmdx/task_hook.rb +18 -0
  83. data/lib/cmdx/utils/datetime_formatter.rb +17 -0
  84. data/lib/cmdx/utils/method_name.rb +24 -0
  85. data/lib/cmdx/utils/runtime.rb +19 -0
  86. data/lib/cmdx/validators/custom.rb +20 -0
  87. data/lib/cmdx/validators/exclusion.rb +51 -0
  88. data/lib/cmdx/validators/format.rb +27 -0
  89. data/lib/cmdx/validators/inclusion.rb +51 -0
  90. data/lib/cmdx/validators/length.rb +114 -0
  91. data/lib/cmdx/validators/numeric.rb +114 -0
  92. data/lib/cmdx/validators/presence.rb +27 -0
  93. data/lib/cmdx/version.rb +7 -0
  94. data/lib/cmdx.rb +80 -0
  95. data/lib/generators/cmdx/batch_generator.rb +30 -0
  96. data/lib/generators/cmdx/install_generator.rb +15 -0
  97. data/lib/generators/cmdx/task_generator.rb +30 -0
  98. data/lib/generators/cmdx/templates/batch.rb.tt +7 -0
  99. data/lib/generators/cmdx/templates/install.rb +23 -0
  100. data/lib/generators/cmdx/templates/task.rb.tt +9 -0
  101. data/lib/locales/en.yml +36 -0
  102. data/lib/locales/es.yml +36 -0
  103. metadata +288 -0
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ module ResultSerializer
5
+
6
+ STRIP_FAILURE = proc do |h, r, k|
7
+ unless r.send(:"#{k}?")
8
+ # Strip caused/threw failures since its the same info as the log line
9
+ h[k] = r.send(k).to_h.except(:caused_failure, :threw_failure)
10
+ end
11
+ end.freeze
12
+
13
+ module_function
14
+
15
+ def call(result)
16
+ {
17
+ index: result.index,
18
+ run_id: result.run.id,
19
+ type: result.task.is_a?(Batch) ? "Batch" : "Task",
20
+ class: result.task.class.name,
21
+ id: result.task.id,
22
+ state: result.state,
23
+ status: result.status,
24
+ outcome: result.outcome,
25
+ metadata: result.metadata,
26
+ runtime: result.runtime,
27
+ tags: result.task.task_setting(:tags),
28
+ pid: Process.pid
29
+ }.tap do |hash|
30
+ if result.failed?
31
+ STRIP_FAILURE.call(hash, result, :caused_failure)
32
+ STRIP_FAILURE.call(hash, result, :threw_failure)
33
+ end
34
+ end
35
+ end
36
+
37
+ end
38
+ end
data/lib/cmdx/run.rb ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ class Run
5
+
6
+ __cmdx_attr_delegator :index, to: :results
7
+ __cmdx_attr_delegator :state, :status, :outcome, :runtime, to: :first_result
8
+
9
+ attr_reader :id, :results
10
+
11
+ def initialize(attributes = {})
12
+ @id = attributes[:id] || SecureRandom.uuid
13
+ @results = Array(attributes[:results])
14
+ end
15
+
16
+ def to_h
17
+ RunSerializer.call(self)
18
+ end
19
+
20
+ def to_s
21
+ RunInspector.call(self)
22
+ end
23
+
24
+ private
25
+
26
+ def first_result
27
+ return @first_result if defined?(@first_result)
28
+
29
+ @first_result = @results.first
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ module RunInspector
5
+
6
+ ORDERED_KEYS = %i[
7
+ state status outcome runtime
8
+ ].freeze
9
+
10
+ module_function
11
+
12
+ def call(run)
13
+ header = "Run: #{run.id}"
14
+ footer = ORDERED_KEYS.map { |key| "#{key}=#{run.send(key)}" }.join(" ")
15
+ spacer = "=" * [header.size, footer.size].max
16
+
17
+ run.results.map(&:to_s).unshift(header, spacer).push(spacer, footer).join("\n")
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ module RunSerializer
5
+
6
+ module_function
7
+
8
+ def call(run)
9
+ {
10
+ id: run.id,
11
+ results: run.results.map(&:to_h)
12
+ }
13
+ end
14
+
15
+ end
16
+ end
data/lib/cmdx/task.rb ADDED
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ class Task
5
+
6
+ HOOKS = [
7
+ :before_validation,
8
+ :after_validation,
9
+ :before_execution,
10
+ :after_execution,
11
+ *Result::STATUSES.map { |s| :"on_#{s}" },
12
+ *Result::STATES.map { |s| :"on_#{s}" }
13
+ ].freeze
14
+
15
+ __cmdx_attr_setting :task_settings, default: -> { CMDx.configuration.to_h.merge(tags: []) }
16
+ __cmdx_attr_setting :cmd_parameters, default: -> { Parameters.new }
17
+ __cmdx_attr_setting :cmd_hooks, default: {}
18
+
19
+ __cmdx_attr_delegator :task_setting, :task_setting?, to: :class
20
+ __cmdx_attr_delegator :skip!, :fail!, :throw!, to: :result
21
+
22
+ attr_reader :id, :errors, :context, :result, :run
23
+ alias ctx context
24
+ alias res result
25
+
26
+ private_class_method :new
27
+
28
+ def initialize(context = {})
29
+ @id = SecureRandom.uuid
30
+ @errors = Errors.new
31
+ @context = Context.build(context)
32
+ @run = @context.run || begin
33
+ run = Run.new(@context.delete!(:run).to_h)
34
+ @context.instance_variable_set(:@run, run)
35
+ end
36
+ @run.results << @result = Result.new(self)
37
+ end
38
+
39
+ class << self
40
+
41
+ HOOKS.each do |hook|
42
+ define_method(hook) do |*callables, **options, &block|
43
+ callables << block if block_given?
44
+ (cmd_hooks[hook] ||= []).push([callables, options]).uniq!
45
+ end
46
+ end
47
+
48
+ def task_setting(key)
49
+ __cmdx_yield(task_settings[key])
50
+ end
51
+
52
+ def task_setting?(key)
53
+ task_settings.key?(key)
54
+ end
55
+
56
+ def task_settings!(**options)
57
+ task_settings.merge!(options)
58
+ end
59
+
60
+ def optional(*attributes, **options, &)
61
+ parameters = Parameter.optional(*attributes, **options.merge(klass: self), &)
62
+ cmd_parameters.concat(parameters)
63
+ end
64
+
65
+ def required(*attributes, **options, &)
66
+ parameters = Parameter.required(*attributes, **options.merge(klass: self), &)
67
+ cmd_parameters.concat(parameters)
68
+ end
69
+
70
+ def call(...)
71
+ instance = send(:new, ...)
72
+ instance.send(:execute_call)
73
+ instance.result
74
+ end
75
+
76
+ def call!(...)
77
+ instance = send(:new, ...)
78
+ instance.send(:execute_call!)
79
+ instance.result
80
+ end
81
+
82
+ end
83
+
84
+ def call
85
+ raise UndefinedCallError, "call method not defined in #{self.class.name}"
86
+ end
87
+
88
+ private
89
+
90
+ def logger
91
+ Logger.call(self)
92
+ end
93
+
94
+ def before_call
95
+ TaskHook.call(self, :before_execution)
96
+
97
+ result.executing!
98
+ TaskHook.call(self, :on_executing)
99
+
100
+ TaskHook.call(self, :before_validation)
101
+ ParameterValidator.call(self)
102
+ TaskHook.call(self, :after_validation)
103
+ end
104
+
105
+ def after_call
106
+ result.send(result.success? ? :complete! : :interrupt!)
107
+ TaskHook.call(self, :"on_#{result.status}")
108
+ TaskHook.call(self, :"on_#{result.state}")
109
+
110
+ TaskHook.call(self, :after_execution)
111
+ end
112
+
113
+ def terminate_call
114
+ Immutator.call(self)
115
+ ResultLogger.call(result)
116
+ end
117
+
118
+ def execute_call
119
+ result.runtime do
120
+ before_call
121
+ call
122
+ rescue UndefinedCallError => e
123
+ raise(e)
124
+ rescue StandardError => e
125
+ result.fail!(reason: "[#{e.class}] #{e.message}", original_exception: e) unless e.is_a?(Fault)
126
+ ensure
127
+ after_call
128
+ end
129
+
130
+ terminate_call
131
+ end
132
+
133
+ def execute_call!
134
+ result.runtime do
135
+ before_call
136
+ call
137
+ rescue UndefinedCallError => e
138
+ raise(e)
139
+ rescue Fault => e
140
+ raise(e) if Array(task_setting(:task_halt)).include?(e.result.status)
141
+
142
+ after_call # HACK: treat as NO-OP
143
+ else
144
+ after_call # ELSE: treat as success
145
+ end
146
+
147
+ terminate_call
148
+ end
149
+
150
+ end
151
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ module TaskHook
5
+
6
+ module_function
7
+
8
+ def call(task, hook)
9
+ Array(task.class.cmd_hooks[hook]).each do |callables, options|
10
+ next unless task.__cmdx_eval(options)
11
+
12
+ hooks = Array(callables)
13
+ hooks.each { |h| task.__cmdx_try(h) }
14
+ end
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ module Utils
5
+ module DatetimeFormatter
6
+
7
+ DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%6N"
8
+
9
+ module_function
10
+
11
+ def call(time)
12
+ time.strftime(DATETIME_FORMAT)
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ module Utils
5
+ module MethodName
6
+
7
+ ROOTFIX = proc do |o, &block|
8
+ o == true ? block.call : o
9
+ end.freeze
10
+
11
+ module_function
12
+
13
+ def call(method_name, source, options = {})
14
+ options[:as] || begin
15
+ prefix = ROOTFIX.call(options[:prefix]) { "#{source}_" }
16
+ suffix = ROOTFIX.call(options[:suffix]) { "_#{source}" }
17
+
18
+ "#{prefix}#{method_name}#{suffix}".strip.to_sym
19
+ end
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ module Utils
5
+ module Runtime
6
+
7
+ module_function
8
+
9
+ def call(&)
10
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
11
+ yield
12
+ finish = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
13
+
14
+ finish - start
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ module Validators
5
+ module Custom
6
+
7
+ module_function
8
+
9
+ def call(value, options = {})
10
+ return if options.dig(:custom, :validator).call(value, options)
11
+
12
+ raise ValidationError, options.dig(:custom, :message) || I18n.t(
13
+ "cmdx.validators.custom",
14
+ default: "is not valid"
15
+ )
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ module Validators
5
+ module Exclusion
6
+
7
+ extend self
8
+
9
+ def call(value, options = {})
10
+ values = options.dig(:exclusion, :in) ||
11
+ options.dig(:exclusion, :within)
12
+
13
+ if values.is_a?(Range)
14
+ raise_within_validation_error!(values.begin, values.end, options) if values.cover?(value)
15
+ elsif Array(values).any? { |v| v === value } # rubocop:disable Style/CaseEquality
16
+ raise_of_validation_error!(values, options)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def raise_of_validation_error!(values, options)
23
+ values = values.map(&:inspect).join(", ")
24
+ message = options.dig(:exclusion, :of_message) ||
25
+ options.dig(:exclusion, :message)
26
+ message %= { values: } unless message.nil?
27
+
28
+ raise ValidationError, message || I18n.t(
29
+ "cmdx.validators.exclusion.of",
30
+ values:,
31
+ default: "must not be one of: #{values}"
32
+ )
33
+ end
34
+
35
+ def raise_within_validation_error!(min, max, options)
36
+ message = options.dig(:exclusion, :in_message) ||
37
+ options.dig(:exclusion, :within_message) ||
38
+ options.dig(:exclusion, :message)
39
+ message %= { min:, max: } unless message.nil?
40
+
41
+ raise ValidationError, message || I18n.t(
42
+ "cmdx.validators.exclusion.within",
43
+ min:,
44
+ max:,
45
+ default: "must not be within #{min} and #{max}"
46
+ )
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ module Validators
5
+ module Format
6
+
7
+ module_function
8
+
9
+ def call(value, options = {})
10
+ return if case options[:format]
11
+ in { with: with, without: without }
12
+ value.match?(with) && !value.match?(without)
13
+ in { with: with }
14
+ value.match?(with)
15
+ in { without: without }
16
+ !value.match?(without)
17
+ end
18
+
19
+ raise ValidationError, options.dig(:format, :message) || I18n.t(
20
+ "cmdx.validators.format",
21
+ default: "is an invalid format"
22
+ )
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ module Validators
5
+ module Inclusion
6
+
7
+ extend self
8
+
9
+ def call(value, options = {})
10
+ values = options.dig(:inclusion, :in) ||
11
+ options.dig(:inclusion, :within)
12
+
13
+ if values.is_a?(Range)
14
+ raise_within_validation_error!(values.begin, values.end, options) unless values.cover?(value)
15
+ elsif Array(values).none? { |v| v === value } # rubocop:disable Style/CaseEquality
16
+ raise_of_validation_error!(values, options)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def raise_of_validation_error!(values, options)
23
+ values = values.map(&:inspect).join(", ")
24
+ message = options.dig(:inclusion, :of_message) ||
25
+ options.dig(:inclusion, :message)
26
+ message %= { values: } unless message.nil?
27
+
28
+ raise ValidationError, message || I18n.t(
29
+ "cmdx.validators.inclusion.of",
30
+ values:,
31
+ default: "must be one of: #{values}"
32
+ )
33
+ end
34
+
35
+ def raise_within_validation_error!(min, max, options)
36
+ message = options.dig(:inclusion, :in_message) ||
37
+ options.dig(:inclusion, :within_message) ||
38
+ options.dig(:inclusion, :message)
39
+ message %= { min:, max: } unless message.nil?
40
+
41
+ raise ValidationError, message || I18n.t(
42
+ "cmdx.validators.inclusion.within",
43
+ min:,
44
+ max:,
45
+ default: "must be within #{min} and #{max}"
46
+ )
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ module Validators
5
+ module Length
6
+
7
+ extend self
8
+
9
+ def call(value, options = {})
10
+ case options[:length]
11
+ in { within: within }
12
+ raise_within_validation_error!(within.begin, within.end, options) unless within.cover?(value.length)
13
+ in { not_within: not_within }
14
+ raise_not_within_validation_error!(not_within.begin, not_within.end, options) if not_within.cover?(value.length)
15
+ in { in: yn }
16
+ raise_within_validation_error!(yn.begin, yn.end, options) unless yn.cover?(value.length)
17
+ in { not_in: not_in }
18
+ raise_not_within_validation_error!(not_in.begin, not_in.end, options) if not_in.cover?(value.length)
19
+ in { min: min, max: max }
20
+ raise_within_validation_error!(min, max, options) unless value.length.between?(min, max)
21
+ in { min: min }
22
+ raise_min_validation_error!(min, options) unless min <= value.length
23
+ in { max: max }
24
+ raise_max_validation_error!(max, options) unless value.length <= max
25
+ in { is: is }
26
+ raise_is_validation_error!(is, options) unless value.length == is
27
+ in { is_not: is_not }
28
+ raise_is_not_validation_error!(is_not, options) if value.length == is_not
29
+ else
30
+ raise ArgumentError, "no known length validator options given"
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def raise_within_validation_error!(min, max, options)
37
+ message = options.dig(:length, :within_message) ||
38
+ options.dig(:length, :in_message) ||
39
+ options.dig(:length, :message)
40
+ message %= { min:, max: } unless message.nil?
41
+
42
+ raise ValidationError, message || I18n.t(
43
+ "cmdx.validators.length.within",
44
+ min:,
45
+ max:,
46
+ default: "length must be within #{min} and #{max}"
47
+ )
48
+ end
49
+
50
+ def raise_not_within_validation_error!(min, max, options)
51
+ message = options.dig(:length, :not_within_message) ||
52
+ options.dig(:length, :not_in_message) ||
53
+ options.dig(:length, :message)
54
+ message %= { min:, max: } unless message.nil?
55
+
56
+ raise ValidationError, message || I18n.t(
57
+ "cmdx.validators.length.not_within",
58
+ min:,
59
+ max:,
60
+ default: "length must not be within #{min} and #{max}"
61
+ )
62
+ end
63
+
64
+ def raise_min_validation_error!(min, options)
65
+ message = options.dig(:length, :min_message) ||
66
+ options.dig(:length, :message)
67
+ message %= { min: } unless message.nil?
68
+
69
+ raise ValidationError, message || I18n.t(
70
+ "cmdx.validators.length.min",
71
+ min:,
72
+ default: "length must be at least #{min}"
73
+ )
74
+ end
75
+
76
+ def raise_max_validation_error!(max, options)
77
+ message = options.dig(:length, :max_message) ||
78
+ options.dig(:length, :message)
79
+ message %= { max: } unless message.nil?
80
+
81
+ raise ValidationError, message || I18n.t(
82
+ "cmdx.validators.length.max",
83
+ max:,
84
+ default: "length must be at most #{max}"
85
+ )
86
+ end
87
+
88
+ def raise_is_validation_error!(is, options)
89
+ message = options.dig(:length, :is_message) ||
90
+ options.dig(:length, :message)
91
+ message %= { is: } unless message.nil?
92
+
93
+ raise ValidationError, message || I18n.t(
94
+ "cmdx.validators.length.is",
95
+ is:,
96
+ default: "length must be #{is}"
97
+ )
98
+ end
99
+
100
+ def raise_is_not_validation_error!(is_not, options)
101
+ message = options.dig(:length, :is_not_message) ||
102
+ options.dig(:length, :message)
103
+ message %= { is_not: } unless message.nil?
104
+
105
+ raise ValidationError, message || I18n.t(
106
+ "cmdx.validators.length.is_not",
107
+ is_not:,
108
+ default: "length must not be #{is_not}"
109
+ )
110
+ end
111
+
112
+ end
113
+ end
114
+ end