active_date_range 0.5.1 → 0.5.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ca443decbae6f02f3b89ec4177942c925eb20fb00fe70b809d96d0573a4d332a
4
- data.tar.gz: 915e82bd0b6449839876d7a193ad2430bd76bb810bedf31cd8b45c6c0abdabbd
3
+ metadata.gz: fa7f0092dab70e955754434aa37df4f7c0c11c5461d54b377c68d935ad115330
4
+ data.tar.gz: 44658f76ff5dca0336804ce7b7dc44fee1b96699365bd04e8bbf93741ff77f75
5
5
  SHA512:
6
- metadata.gz: ff38e2d4be8755802f6ef51cb487366bd81341dd62948b38e463f6c583920fd5c4ec344052f3d9a0588bd60c8d04b25a12a41c8f0020dca3f80d879daea59120
7
- data.tar.gz: e9dc0f34eca52082773c9c6fa2a4609af3955a4731c56ded61a5d94af357185348f98b724b029eb4b5e31e08469c27c6f860e7b07bd4ef712a49cf42be6247a3
6
+ metadata.gz: e7363eeba6c6e8747afd3e3f576fe2d3b24fbe03b17b30de980458fe4e65b9a13a31344a31207d9ae09fe8d0f7a7e97589e6c697f9564fa7977bbf227e80ac01
7
+ data.tar.gz: 5a3d15754ce33007ae78e56a0790ac5ede08f34a043d72e0e9866f61f912b2ebec9081af7df285927f83b3d6fd762c20bb4cbb11ae0bf411b7499966f7306e6e
data/CHANGELOG.md CHANGED
@@ -1,3 +1,41 @@
1
+ ## 0.5.3
2
+
3
+ * Add `safe_parse`, which returns `nil` instead of raising for input that isn't a valid date range. Use it for input you don't control, like a query parameter or a form field:
4
+
5
+ ```ruby
6
+ DateRange.safe_parse("202101..202112") # => DateRange(2021-01-01..2021-12-31)
7
+ DateRange.safe_parse("test") # => nil
8
+ DateRange.safe_parse("202112..202101") # => nil
9
+ ```
10
+
11
+ *wesselmoneybird*
12
+
13
+ * Add a `safe: true` option to the `:date_range` ActiveModel type, which casts to `nil` instead of raising. Use it for attributes fed by input you don't control, so a validation can report the problem:
14
+
15
+ ```ruby
16
+ attribute :period, :date_range, safe: true
17
+ ```
18
+
19
+ *wesselmoneybird*
20
+
21
+ ## 0.5.2
22
+
23
+ * Add calendar-aware `DateRangeValidator` for ActiveModel validations. Unlike `validates_length_of` which compares day counts (causing `1.month` to use ~30.44 day averages), this validator uses date arithmetic so that February (28 or 29 days) correctly satisfies `minimum_duration: 1.month`:
24
+
25
+ ```ruby
26
+ validates :period, date_range: { minimum_duration: 1.month }
27
+ validates :period, date_range: { maximum_duration: 1.year }
28
+ validates :period, date_range: { exact_duration: 3.months }
29
+ validates :period, date_range: { duration: 1.month..1.year }
30
+ validates :period, date_range: { bounded: true }
31
+ validates :period, date_range: { full_periods_of: :month }
32
+ validates :period, date_range: { starts_on: :beginning_of_month }
33
+ validates :period, date_range: { ends_on: :end_of_month }
34
+ validates :period, date_range: { covers: -> { Date.today } }
35
+ ```
36
+
37
+ *Edwin Vlieg*
38
+
1
39
  ## 0.5.1
2
40
 
3
41
  * Return `Float::INFINITY` from `size` for boundless ranges, making `validates_length_of` correctly reject them.
data/Gemfile.lock CHANGED
@@ -1,16 +1,16 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- active_date_range (0.5.1)
4
+ active_date_range (0.5.3)
5
5
  activesupport (~> 8.0)
6
6
  i18n (~> 1.6)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- actionpack (8.0.3)
12
- actionview (= 8.0.3)
13
- activesupport (= 8.0.3)
11
+ actionpack (8.1.2)
12
+ actionview (= 8.1.2)
13
+ activesupport (= 8.1.2)
14
14
  nokogiri (>= 1.8.5)
15
15
  rack (>= 2.2.4)
16
16
  rack-session (>= 1.0.1)
@@ -18,22 +18,22 @@ GEM
18
18
  rails-dom-testing (~> 2.2)
19
19
  rails-html-sanitizer (~> 1.6)
20
20
  useragent (~> 0.16)
21
- actionview (8.0.3)
22
- activesupport (= 8.0.3)
21
+ actionview (8.1.2)
22
+ activesupport (= 8.1.2)
23
23
  builder (~> 3.1)
24
24
  erubi (~> 1.11)
25
25
  rails-dom-testing (~> 2.2)
26
26
  rails-html-sanitizer (~> 1.6)
27
- activemodel (8.0.3)
28
- activesupport (= 8.0.3)
29
- activesupport (8.0.3)
27
+ activemodel (8.1.2)
28
+ activesupport (= 8.1.2)
29
+ activesupport (8.1.2)
30
30
  base64
31
- benchmark (>= 0.3)
32
31
  bigdecimal
33
32
  concurrent-ruby (~> 1.0, >= 1.3.1)
34
33
  connection_pool (>= 2.2.5)
35
34
  drb
36
35
  i18n (>= 1.6, < 2)
36
+ json
37
37
  logger (>= 1.4.2)
38
38
  minitest (>= 5.1)
39
39
  securerandom (>= 0.3)
@@ -41,102 +41,102 @@ GEM
41
41
  uri (>= 0.13.1)
42
42
  ast (2.4.3)
43
43
  base64 (0.3.0)
44
- benchmark (0.4.1)
45
44
  benchmark-ips (2.14.0)
46
- bigdecimal (3.3.0)
45
+ bigdecimal (4.0.1)
47
46
  builder (3.3.0)
48
47
  coderay (1.1.3)
49
- concurrent-ruby (1.3.5)
50
- connection_pool (2.5.4)
51
- crass (1.0.6)
52
- date (3.4.1)
48
+ concurrent-ruby (1.3.7)
49
+ connection_pool (3.0.2)
50
+ crass (1.0.7)
51
+ date (3.5.1)
53
52
  drb (2.2.3)
54
- erb (5.0.3)
53
+ erb (6.0.1)
55
54
  erubi (1.13.1)
56
- ffi (1.17.2)
57
- ffi (1.17.2-x86_64-linux-gnu)
58
- formatador (1.2.1)
55
+ ffi (1.17.3)
56
+ ffi (1.17.3-x86_64-linux-gnu)
57
+ formatador (1.2.3)
59
58
  reline
60
- guard (2.19.1)
59
+ guard (2.20.1)
61
60
  formatador (>= 0.2.4)
62
61
  listen (>= 2.7, < 4.0)
63
62
  logger (~> 1.6)
64
63
  lumberjack (>= 1.0.12, < 2.0)
65
64
  nenv (~> 0.1)
66
65
  notiffany (~> 0.0)
67
- ostruct (~> 0.6)
68
66
  pry (>= 0.13.0)
69
67
  shellany (~> 0.0)
70
68
  thor (>= 0.18.1)
71
69
  guard-compat (1.2.1)
72
- guard-minitest (2.4.6)
70
+ guard-minitest (3.0.0)
73
71
  guard-compat (~> 1.2)
74
- minitest (>= 3.0)
75
- i18n (1.14.7)
72
+ minitest (>= 5.0.4, < 7.0)
73
+ i18n (1.14.8)
76
74
  concurrent-ruby (~> 1.0)
77
- io-console (0.8.1)
78
- irb (1.15.2)
75
+ io-console (0.8.2)
76
+ irb (1.17.0)
79
77
  pp (>= 0.6.0)
78
+ prism (>= 1.3.0)
80
79
  rdoc (>= 4.0.0)
81
80
  reline (>= 0.4.2)
82
- json (2.15.1)
81
+ json (2.18.1)
83
82
  language_server-protocol (3.17.0.5)
84
83
  lint_roller (1.1.0)
85
- listen (3.9.0)
84
+ listen (3.10.0)
85
+ logger
86
86
  rb-fsevent (~> 0.10, >= 0.10.3)
87
87
  rb-inotify (~> 0.9, >= 0.9.10)
88
88
  logger (1.7.0)
89
- loofah (2.24.1)
89
+ loofah (2.25.2)
90
90
  crass (~> 1.0.2)
91
91
  nokogiri (>= 1.12.0)
92
92
  lumberjack (1.4.2)
93
93
  memory_profiler (1.1.0)
94
94
  method_source (1.1.0)
95
95
  mini_portile2 (2.8.9)
96
- minitest (5.25.5)
96
+ minitest (5.27.0)
97
97
  nenv (0.3.0)
98
- nokogiri (1.18.10)
98
+ nokogiri (1.19.4)
99
99
  mini_portile2 (~> 2.8.2)
100
100
  racc (~> 1.4)
101
- nokogiri (1.18.10-x86_64-linux-gnu)
101
+ nokogiri (1.19.4-x86_64-linux-gnu)
102
102
  racc (~> 1.4)
103
103
  notiffany (0.1.3)
104
104
  nenv (~> 0.1)
105
105
  shellany (~> 0.0)
106
- ostruct (0.6.3)
107
106
  parallel (1.27.0)
108
- parser (3.3.9.0)
107
+ parser (3.3.10.2)
109
108
  ast (~> 2.4.1)
110
109
  racc
111
110
  pp (0.6.3)
112
111
  prettyprint
113
112
  prettyprint (0.2.0)
114
- prism (1.5.1)
115
- pry (0.15.2)
113
+ prism (1.9.0)
114
+ pry (0.16.0)
116
115
  coderay (~> 1.1)
117
116
  method_source (~> 1.0)
118
- psych (5.2.6)
117
+ reline (>= 0.6.0)
118
+ psych (5.3.1)
119
119
  date
120
120
  stringio
121
121
  racc (1.8.1)
122
- rack (3.2.2)
122
+ rack (3.2.5)
123
123
  rack-session (2.1.1)
124
124
  base64 (>= 0.1.0)
125
125
  rack (>= 3.0.0)
126
126
  rack-test (2.2.0)
127
127
  rack (>= 1.3)
128
- rackup (2.2.1)
128
+ rackup (2.3.1)
129
129
  rack (>= 3)
130
130
  rails-dom-testing (2.3.0)
131
131
  activesupport (>= 5.0.0)
132
132
  minitest
133
133
  nokogiri (>= 1.6)
134
- rails-html-sanitizer (1.6.2)
135
- loofah (~> 2.21)
134
+ rails-html-sanitizer (1.7.1)
135
+ loofah (~> 2.25, >= 2.25.2)
136
136
  nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
137
- railties (8.0.3)
138
- actionpack (= 8.0.3)
139
- activesupport (= 8.0.3)
137
+ railties (8.1.2)
138
+ actionpack (= 8.1.2)
139
+ activesupport (= 8.1.2)
140
140
  irb (~> 1.13)
141
141
  rackup (>= 1.0.0)
142
142
  rake (>= 12.2)
@@ -144,18 +144,18 @@ GEM
144
144
  tsort (>= 0.2)
145
145
  zeitwerk (~> 2.6)
146
146
  rainbow (3.1.1)
147
- rake (13.3.0)
147
+ rake (13.3.1)
148
148
  rb-fsevent (0.11.2)
149
149
  rb-inotify (0.11.1)
150
150
  ffi (~> 1.0)
151
- rdoc (6.15.0)
151
+ rdoc (7.2.0)
152
152
  erb
153
153
  psych (>= 4.0.0)
154
154
  tsort
155
155
  regexp_parser (2.11.3)
156
- reline (0.6.2)
156
+ reline (0.6.3)
157
157
  io-console (~> 0.5)
158
- rubocop (1.81.1)
158
+ rubocop (1.84.2)
159
159
  json (~> 2.3)
160
160
  language_server-protocol (~> 3.17.0.2)
161
161
  lint_roller (~> 1.1.0)
@@ -163,20 +163,20 @@ GEM
163
163
  parser (>= 3.3.0.2)
164
164
  rainbow (>= 2.2.2, < 4.0)
165
165
  regexp_parser (>= 2.9.3, < 3.0)
166
- rubocop-ast (>= 1.47.1, < 2.0)
166
+ rubocop-ast (>= 1.49.0, < 2.0)
167
167
  ruby-progressbar (~> 1.7)
168
168
  unicode-display_width (>= 2.4.0, < 4.0)
169
- rubocop-ast (1.47.1)
169
+ rubocop-ast (1.49.0)
170
170
  parser (>= 3.3.7.2)
171
- prism (~> 1.4)
171
+ prism (~> 1.7)
172
172
  rubocop-packaging (0.6.0)
173
173
  lint_roller (~> 1.1.0)
174
174
  rubocop (>= 1.72.1, < 2.0)
175
- rubocop-performance (1.26.0)
175
+ rubocop-performance (1.26.1)
176
176
  lint_roller (~> 1.1)
177
177
  rubocop (>= 1.75.0, < 2.0)
178
- rubocop-ast (>= 1.44.0, < 2.0)
179
- rubocop-rails (2.33.4)
178
+ rubocop-ast (>= 1.47.1, < 2.0)
179
+ rubocop-rails (2.34.3)
180
180
  activesupport (>= 4.2.0)
181
181
  lint_roller (~> 1.1)
182
182
  rack (>= 1.1)
@@ -185,17 +185,17 @@ GEM
185
185
  ruby-progressbar (1.13.0)
186
186
  securerandom (0.4.1)
187
187
  shellany (0.0.1)
188
- stringio (3.1.7)
189
- thor (1.4.0)
188
+ stringio (3.2.0)
189
+ thor (1.5.0)
190
190
  tsort (0.2.0)
191
191
  tzinfo (2.0.6)
192
192
  concurrent-ruby (~> 1.0)
193
193
  unicode-display_width (3.2.0)
194
194
  unicode-emoji (~> 4.1)
195
- unicode-emoji (4.1.0)
196
- uri (1.0.4)
195
+ unicode-emoji (4.2.0)
196
+ uri (1.1.1)
197
197
  useragent (0.16.11)
198
- zeitwerk (2.7.3)
198
+ zeitwerk (2.7.5)
199
199
 
200
200
  PLATFORMS
201
201
  ruby
@@ -218,4 +218,4 @@ DEPENDENCIES
218
218
  rubocop-rails (~> 2.33)
219
219
 
220
220
  BUNDLED WITH
221
- 2.3.5
221
+ 2.6.9
data/README.md CHANGED
@@ -55,6 +55,14 @@ ActiveDateRange::DateRange.parse('20210101..20210115')
55
55
 
56
56
  Parse accepts three formats: `YYYYMM..YYYYMM`, `YYYYMMDD..YYYYMMDD` and any short hand like `this_month`.
57
57
 
58
+ `parse` raises `InvalidDateRangeFormat` or `InvalidDateRange` on input it can't handle. For input you don't control, like a query parameter or a form field, use `safe_parse` instead. It returns `nil` in those cases, the same way `safe_constantize` relates to `constantize`:
59
+
60
+ ```ruby
61
+ ActiveDateRange::DateRange.safe_parse('202101..202112') # => DateRange(2021-01-01..2021-12-31)
62
+ ActiveDateRange::DateRange.safe_parse('test') # => nil
63
+ ActiveDateRange::DateRange.safe_parse('202112..202101') # => nil
64
+ ```
65
+
58
66
  ### The date range instance
59
67
 
60
68
  A `DateRange` object is an extension of a regular `Range` object. You can use [all methods available on `Range`](https://ruby-doc.org/core-3.0.0/Range.html):
@@ -126,6 +134,21 @@ class Report
126
134
  end
127
135
  ```
128
136
 
137
+ The attribute raises on a value it can't parse. For an attribute fed by input you don't control, like a query parameter or a form field, pass `safe: true`. The attribute then casts to `nil` instead, so a validation can report the problem:
138
+
139
+ ```ruby
140
+ class ReportFilter
141
+ include ActiveModel::Attributes
142
+ include ActiveModel::Validations
143
+
144
+ attribute :period, :date_range, safe: true
145
+
146
+ validates :period, date_range: { bounded: true }
147
+ end
148
+
149
+ ReportFilter.new(period: "test").period # => nil
150
+ ```
151
+
129
152
  ### Usage example
130
153
 
131
154
  Use the shorthands to link to a specific period:
@@ -2,8 +2,17 @@
2
2
 
3
3
  module ActiveDateRange
4
4
  class DateRangeType < ActiveModel::Type::String
5
+ # Pass <tt>safe: true</tt> for attributes fed by input you don't control, like a query
6
+ # parameter. The attribute then casts to nil instead of raising:
7
+ #
8
+ # attribute :period, :date_range, safe: true
9
+ def initialize(safe: false)
10
+ @safe = safe
11
+ super()
12
+ end
13
+
5
14
  def cast(value)
6
- ActiveDateRange::DateRange.parse(value)
15
+ @safe ? ActiveDateRange::DateRange.safe_parse(value) : ActiveDateRange::DateRange.parse(value)
7
16
  end
8
17
  end
9
18
  end
@@ -41,6 +41,19 @@ module ActiveDateRange
41
41
  DateRange.new(parse_date(begin_date), parse_date(end_date, last: true))
42
42
  end
43
43
 
44
+ # Parses a date range string like #parse, but returns nil when the input isn't a valid date
45
+ # range, the same way <tt>safe_constantize</tt> relates to <tt>constantize</tt>. Use this for
46
+ # input you don't control, like a query parameter or a form field.
47
+ #
48
+ # DateRange.safe_parse("202101..202112") # => DateRange(2021-01-01..2021-12-31)
49
+ # DateRange.safe_parse("test") # => nil
50
+ # DateRange.safe_parse("202112..202101") # => nil
51
+ def self.safe_parse(input)
52
+ parse(input)
53
+ rescue InvalidDateRangeFormat, InvalidDateRange
54
+ nil
55
+ end
56
+
44
57
  def self.parse_date(input, last: false)
45
58
  return if input.blank?
46
59
 
@@ -39,3 +39,14 @@ en:
39
39
  long_quarter: "quarter %{quarter}"
40
40
  short_quarter: "Q%{quarter}"
41
41
  range: "%{begin} - %{end}"
42
+ errors:
43
+ messages:
44
+ not_a_date_range: "is not a valid date range"
45
+ unbounded: "must have both a start and end date"
46
+ duration_too_short: "is too short (minimum duration is %{duration})"
47
+ duration_too_long: "is too long (maximum duration is %{duration})"
48
+ wrong_duration: "has the wrong duration (should be %{duration})"
49
+ not_full_periods: "must consist of full %{period}s"
50
+ misaligned_start: "must start at the %{alignment}"
51
+ misaligned_end: "must end at the %{alignment}"
52
+ does_not_cover: "must include %{date}"
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveDateRange
4
+ module Validators
5
+ class DateRangeValidator < ActiveModel::EachValidator
6
+ include ResolveValue
7
+
8
+ DURATION_OPTIONS = %i[minimum_duration maximum_duration exact_duration duration].freeze
9
+ GRANULARITY_MAP = {
10
+ month: :full_month?,
11
+ quarter: :full_quarter?,
12
+ year: :full_year?,
13
+ week: :full_week?
14
+ }.freeze
15
+ START_ALIGNMENT_MAP = {
16
+ beginning_of_month: :begin_at_beginning_of_month?,
17
+ beginning_of_quarter: :begin_at_beginning_of_quarter?,
18
+ beginning_of_year: :begin_at_beginning_of_year?,
19
+ beginning_of_week: :begin_at_beginning_of_week?
20
+ }.freeze
21
+ END_ALIGNMENT_MAP = {
22
+ end_of_month: ->(r) { r.end == r.end.at_end_of_month },
23
+ end_of_quarter: ->(r) { r.end == r.end.at_end_of_quarter },
24
+ end_of_year: ->(r) { r.end == r.end.at_end_of_year },
25
+ end_of_week: ->(r) { r.end == r.end.at_end_of_week }
26
+ }.freeze
27
+
28
+ def validate_each(record, attribute, value)
29
+ unless value.is_a?(ActiveDateRange::DateRange)
30
+ record.errors.add(attribute, :not_a_date_range, **options.except(*known_options))
31
+ return
32
+ end
33
+
34
+ validate_bounded(record, attribute, value)
35
+ validate_duration(record, attribute, value)
36
+ validate_full_periods_of(record, attribute, value)
37
+ validate_starts_on(record, attribute, value)
38
+ validate_ends_on(record, attribute, value)
39
+ validate_covers(record, attribute, value)
40
+ end
41
+
42
+ private
43
+ def known_options
44
+ DURATION_OPTIONS + %i[bounded full_periods_of starts_on ends_on covers message]
45
+ end
46
+
47
+ def validate_bounded(record, attribute, value)
48
+ return unless options[:bounded]
49
+
50
+ if value.boundless?
51
+ record.errors.add(attribute, :unbounded, **options.except(*known_options))
52
+ end
53
+ end
54
+
55
+ def validate_duration(record, attribute, value)
56
+ return if value.boundless?
57
+
58
+ if options[:duration]
59
+ range = resolve_value(record, options[:duration])
60
+ validate_minimum_duration(record, attribute, value, range.begin) if range.begin
61
+ validate_maximum_duration(record, attribute, value, range.end) if range.end
62
+ end
63
+
64
+ if options[:minimum_duration]
65
+ duration = resolve_value(record, options[:minimum_duration])
66
+ validate_minimum_duration(record, attribute, value, duration)
67
+ end
68
+
69
+ if options[:maximum_duration]
70
+ duration = resolve_value(record, options[:maximum_duration])
71
+ validate_maximum_duration(record, attribute, value, duration)
72
+ end
73
+
74
+ if options[:exact_duration]
75
+ duration = resolve_value(record, options[:exact_duration])
76
+ validate_exact_duration(record, attribute, value, duration)
77
+ end
78
+ end
79
+
80
+ def validate_minimum_duration(record, attribute, value, duration)
81
+ unless meets_minimum_duration?(value, duration)
82
+ record.errors.add(attribute, :duration_too_short,
83
+ duration: humanize_duration(duration),
84
+ **options.except(*known_options))
85
+ end
86
+ end
87
+
88
+ def validate_maximum_duration(record, attribute, value, duration)
89
+ unless meets_maximum_duration?(value, duration)
90
+ record.errors.add(attribute, :duration_too_long,
91
+ duration: humanize_duration(duration),
92
+ **options.except(*known_options))
93
+ end
94
+ end
95
+
96
+ def validate_exact_duration(record, attribute, value, duration)
97
+ unless meets_minimum_duration?(value, duration) && meets_maximum_duration?(value, duration)
98
+ record.errors.add(attribute, :wrong_duration,
99
+ duration: humanize_duration(duration),
100
+ **options.except(*known_options))
101
+ end
102
+ end
103
+
104
+ # Calendar-aware: Feb 1..Feb 28 with 1.month → begin + 1.month - 1.day = Feb 28 <= Feb 28 ✓
105
+ def meets_minimum_duration?(range, duration)
106
+ range.begin + duration - 1.day <= range.end
107
+ end
108
+
109
+ # Calendar-aware: Jan 1..Dec 31 with 1.year → begin + 1.year - 1.day = Dec 31 >= Dec 31 ✓
110
+ def meets_maximum_duration?(range, duration)
111
+ range.begin + duration - 1.day >= range.end
112
+ end
113
+
114
+ def validate_full_periods_of(record, attribute, value)
115
+ return unless options[:full_periods_of]
116
+
117
+ period = options[:full_periods_of].to_sym
118
+ method = GRANULARITY_MAP[period]
119
+
120
+ unless method && value.respond_to?(method) && value.public_send(method)
121
+ record.errors.add(attribute, :not_full_periods,
122
+ period: period,
123
+ **options.except(*known_options))
124
+ end
125
+ end
126
+
127
+ def validate_starts_on(record, attribute, value)
128
+ return unless options[:starts_on]
129
+
130
+ alignment = options[:starts_on].to_sym
131
+ method = START_ALIGNMENT_MAP[alignment]
132
+
133
+ unless method && value.respond_to?(method) && value.public_send(method)
134
+ record.errors.add(attribute, :misaligned_start,
135
+ alignment: alignment.to_s.tr("_", " "),
136
+ **options.except(*known_options))
137
+ end
138
+ end
139
+
140
+ def validate_ends_on(record, attribute, value)
141
+ return unless options[:ends_on]
142
+ return if value.end.nil?
143
+
144
+ alignment = options[:ends_on].to_sym
145
+ checker = END_ALIGNMENT_MAP[alignment]
146
+
147
+ unless checker&.call(value)
148
+ record.errors.add(attribute, :misaligned_end,
149
+ alignment: alignment.to_s.tr("_", " "),
150
+ **options.except(*known_options))
151
+ end
152
+ end
153
+
154
+ def validate_covers(record, attribute, value)
155
+ return unless options[:covers]
156
+
157
+ date = resolve_value(record, options[:covers])
158
+
159
+ unless value.cover?(date)
160
+ record.errors.add(attribute, :does_not_cover,
161
+ date: date,
162
+ **options.except(*known_options))
163
+ end
164
+ end
165
+
166
+ def humanize_duration(duration)
167
+ return duration.inspect unless duration.respond_to?(:parts)
168
+
169
+ duration.parts.map do |unit, amount|
170
+ label = amount == 1 ? unit.to_s.singularize : unit.to_s.pluralize
171
+ "#{amount} #{label}"
172
+ end.join(", ")
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveDateRange
4
+ module Validators
5
+ module ResolveValue
6
+ private
7
+ def resolve_value(record, value)
8
+ case value
9
+ when Proc then value.arity == 0 ? value.call : value.call(record)
10
+ when Symbol then record.send(value)
11
+ else value
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_date_range/validators/resolve_value"
4
+ require "active_date_range/validators/date_range_validator"
5
+
6
+ # Register validator at top level so `validates :x, date_range: {}` works
7
+ ::DateRangeValidator = ActiveDateRange::Validators::DateRangeValidator
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveDateRange
4
- VERSION = "0.5.1"
4
+ VERSION = "0.5.3"
5
5
  end
@@ -12,6 +12,7 @@ require "active_date_range/version"
12
12
  require "active_date_range/date_range"
13
13
  require "active_date_range/humanizer"
14
14
  require "active_date_range/active_model_type"
15
+ require "active_date_range/validators" if defined?(ActiveModel)
15
16
 
16
17
  module ActiveDateRange
17
18
  class Error < StandardError; end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_date_range
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Edwin Vlieg
@@ -139,6 +139,9 @@ files:
139
139
  - lib/active_date_range/humanizer.rb
140
140
  - lib/active_date_range/i18n.rb
141
141
  - lib/active_date_range/locale/en.yml
142
+ - lib/active_date_range/validators.rb
143
+ - lib/active_date_range/validators/date_range_validator.rb
144
+ - lib/active_date_range/validators/resolve_value.rb
142
145
  - lib/active_date_range/version.rb
143
146
  homepage: https://github.com/moneybird/active-date-range
144
147
  licenses: