in_time_scope 0.1.0 → 0.1.5

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: eee76cde212cee461aa7b915b048c5479bc1adc82faa1a57fc4d10e136806eb6
4
- data.tar.gz: b6de2422411c951e2e1cf3029498755b77c9de73731ed9e24d3d20c0df612d79
3
+ metadata.gz: 3938b69e9b635a2d488513fbba9a0af9171e163accf5a776357f837f93783830
4
+ data.tar.gz: eb16bb16cb6681cc8ffa67b10aa32b8e349f5df87734436a6e5d8ff7afebd26a
5
5
  SHA512:
6
- metadata.gz: 9d9daa7dc0815efde3c0d6a0d6231da6799526fa17be1c96bc22cb76f460ce7e4c84f40ae449760a5e24e86177632b174c94ecf13ec0d3240cae9c4a39ad5721
7
- data.tar.gz: 337490e629eca19edec0fe0615ee895f67e475258281a173c61e10cf8014a4f185ae72c48fbc07065d7ae29c0b6e9016b547827b83556a4d8c6be3c79c95ec04
6
+ metadata.gz: 0cea4dc04116f248f8343902d9aeeb875e6a3cac9dc6c772a9462daea7d99a526862d715af5e94fa45826896ab5cd832a55b7f31fb7b1a94e12649a0ea059984
7
+ data.tar.gz: e55bc250bd8d38b6cc7fb5a2b1d81d808948244defe93aa4715fb052967f8b3318d62b5dd58e614b03ddaf528a3ddb7ef4ae07786f9dbc67e2eced0c28065d9d
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 3.1
2
+ TargetRubyVersion: 3.0
3
3
  NewCops: enable
4
4
  SuggestExtensions: false
5
5
 
data/README.md CHANGED
@@ -1,8 +1,17 @@
1
1
  # InTimeScope
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ A Ruby gem that adds time-window scopes to ActiveRecord models. It provides a convenient way to query records that fall within specific time periods (between `start_at` and `end_at` timestamps), with support for nullable columns, custom column names, and multiple scopes per model.
4
4
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/in_time_scope`. To experiment with that code, run `bin/console` for an interactive prompt.
5
+ ## Motivation
6
+
7
+ This gem is inspired by [onk/shibaraku](https://github.com/onk/shibaraku). While shibaraku is a great gem, I wanted to extend it with additional features:
8
+
9
+ - **Nullable column handling**: shibaraku generates SQL with `OR` conditions when columns allow `NULL`, which can impact query performance. InTimeScope auto-detects column nullability from the schema and generates optimized queries.
10
+ - **Named scopes**: shibaraku only provides `in_time` method. InTimeScope allows multiple named scopes like `in_time_published`, `in_time_featured` per model.
11
+ - **Start-only / End-only patterns**: Support for versioned records (start_at only, no end_at) and expiration patterns (end_at only, no start_at).
12
+ - **has_one association support**: `latest_in_time` and `earliest_in_time` scopes optimized for `has_one` with `includes`, using NOT EXISTS subqueries.
13
+
14
+ These features required significant architectural changes, so I created a new gem rather than extending shibaraku.
6
15
 
7
16
  ## Installation
8
17
 
@@ -32,18 +41,15 @@ create_table :events do |t|
32
41
  end
33
42
 
34
43
  class Event < ActiveRecord::Base
35
- include InTimeScope
36
-
37
44
  # Uses start_at / end_at by default
38
45
  in_time_scope
39
46
  end
40
47
 
41
48
  Event.in_time
42
- # => SELECT "events".* FROM "events" WHERE ("events"."start_at" IS NULL OR "events"."start_at" <= '2026-01-24 19:50:05.738232') AND ("events"."end_at" IS NULL OR "events"."end_at" > '2026-01-24 19:50:05.738232') /* loading for pp */ LIMIT $1 [["LIMIT", 11]]
49
+ # => SELECT "events".* FROM "events" WHERE ("events"."start_at" IS NULL OR "events"."start_at" <= '2026-01-24 19:50:05.738232') AND ("events"."end_at" IS NULL OR "events"."end_at" > '2026-01-24 19:50:05.738232')
43
50
 
44
51
  # Check at a specific time
45
52
  Event.in_time(Time.parse("2024-06-01 12:00:00"))
46
- # => SELECT "events".* FROM "events" WHERE ("events"."start_at" IS NULL OR "events"."start_at" <= '2024-06-01 12:00:00.000000') AND ("events"."end_at" IS NULL OR "events"."end_at" > '2024-06-01 12:00:00.000000') /* loading for pp */ LIMIT $1 [["LIMIT", 11]]
47
53
 
48
54
  # Is the current time within the window?
49
55
  event = Event.first
@@ -68,15 +74,13 @@ end
68
74
 
69
75
  # Column metadata is read when Rails boots; SQL is optimized for NOT NULL columns.
70
76
  Event.in_time
71
- # => SELECT "events".* FROM "events" WHERE ("events"."start_at" <= '2026-01-24 19:50:05.738232') AND ("events"."end_at" > '2026-01-24 19:50:05.738232') /* loading for pp */ LIMIT $1 [["LIMIT", 11]]
77
+ # => SELECT "events".* FROM "events" WHERE ("events"."start_at" <= '2026-01-24 19:50:05.738232') AND ("events"."end_at" > '2026-01-24 19:50:05.738232')
72
78
 
73
79
  # Check at a specific time
74
80
  Event.in_time(Time.parse("2024-06-01 12:00:00"))
75
- # => SELECT "events".* FROM "events" WHERE ("events"."start_at" <= '2024-06-01 12:00:00.000000') AND ("events"."end_at" > '2024-06-01 12:00:00.000000') /* loading for pp */ LIMIT $1 [["LIMIT", 11]]
81
+ # => SELECT "events".* FROM "events" WHERE ("events"."start_at" <= '2024-06-01 12:00:00.000000') AND ("events"."end_at" > '2024-06-01 12:00:00.000000')
76
82
 
77
83
  class Event < ActiveRecord::Base
78
- include InTimeScope
79
-
80
84
  # Explicitly mark columns as NOT NULL (even if the DB allows NULL)
81
85
  in_time_scope start_at: { null: false }, end_at: { null: false }
82
86
  end
@@ -87,17 +91,18 @@ Use these options in `in_time_scope` to customize column behavior.
87
91
 
88
92
  | Option | Applies to | Type | Default | Description | Example |
89
93
  | --- | --- | --- | --- | --- | --- |
90
- | `:scope_name` (1st arg) | in_time | `Symbol` | `:in_time` | Creates a named scope like `published_in_time` | `in_time_scope :published` |
94
+ | `:scope_name` (1st arg) | in_time | `Symbol` | `:in_time` | Creates a named scope like `in_time_published` | `in_time_scope :published` |
91
95
  | `start_at: { column: ... }` | start_at | `Symbol` / `nil` | `:start_at` (or `:"<scope>_start_at"` when `:scope_name` is set) | Use a custom column name; set `nil` to disable `start_at` | `start_at: { column: :available_at }` |
92
96
  | `end_at: { column: ... }` | end_at | `Symbol` / `nil` | `:end_at` (or `:"<scope>_end_at"` when `:scope_name` is set) | Use a custom column name; set `nil` to disable `end_at` | `end_at: { column: nil }` |
93
97
  | `start_at: { null: ... }` | start_at | `true/false` | auto (schema) | Force NULL-aware vs NOT NULL behavior | `start_at: { null: false }` |
94
98
  | `end_at: { null: ... }` | end_at | `true/false` | auto (schema) | Force NULL-aware vs NOT NULL behavior | `end_at: { null: true }` |
99
+ | `prefix: true` | scope_name | `true/false` | `false` | Use prefix style method name like `published_in_time` instead of `in_time_published` | `in_time_scope :published, prefix: true` |
95
100
 
96
101
  ### Alternative: Start-Only History (No `end_at`)
97
102
  Use this when periods never overlap and you want exactly one "current" row.
98
103
 
99
- Assumptions:
100
- - `start_at` is always present
104
+ **Requirements:**
105
+ - `start_at` must be NOT NULL (a `ConfigurationError` is raised otherwise)
101
106
  - periods never overlap (validated)
102
107
  - the latest row is the current one
103
108
 
@@ -105,27 +110,21 @@ If your table still has an `end_at` column but you want to ignore it, disable it
105
110
 
106
111
  ```ruby
107
112
  class Event < ActiveRecord::Base
108
- include InTimeScope
109
-
110
113
  # Ignore end_at even if the column exists
111
114
  in_time_scope start_at: { null: false }, end_at: { column: nil }
112
115
  end
113
116
 
114
117
  Event.in_time(Time.parse("2024-06-01 12:00:00"))
115
- # => SELECT "events".* FROM "events" WHERE "events"."start_at" <= '2024-06-01 12:00:00.000000' ORDER BY "events"."start_at" DESC
118
+ # => SELECT "events".* FROM "events" WHERE "events"."start_at" <= '2024-06-01 12:00:00.000000'
116
119
 
117
- # Use .first to get the most recent single record
118
- Event.in_time.first
119
- # => SELECT "events".* FROM "events" WHERE "events"."start_at" <= '...' ORDER BY "events"."start_at" DESC LIMIT 1
120
+ # Use .first with order to get the most recent single record
121
+ Event.in_time.order(start_at: :desc).first
120
122
  ```
121
123
 
122
124
  With no `end_at`, each row implicitly ends at the next row's `start_at`.
123
- The scope returns all matching records ordered by `start_at DESC`, so:
124
- - Use `.first` for a single record
125
- - Use with `has_one` associations for per-parent record selection
126
-
127
- Boundary behavior is stable:
128
- - `start_at <= NOW()` picks the newest row whose start has passed
125
+ The scope returns all matching records (WHERE only, no ORDER), so:
126
+ - Add `.order(start_at: :desc).first` for a single latest record
127
+ - Use `latest_in_time` for efficient `has_one` associations
129
128
 
130
129
  Recommended index:
131
130
 
@@ -136,30 +135,20 @@ CREATE INDEX index_events_on_start_at ON events (start_at);
136
135
  ### Alternative: End-Only Expiration (No `start_at`)
137
136
  Use this when a record is active immediately and expires at `end_at`.
138
137
 
139
- Assumptions:
138
+ **Requirements:**
139
+ - `end_at` must be NOT NULL (a `ConfigurationError` is raised otherwise)
140
140
  - `start_at` is not used (implicit "always active")
141
- - `end_at` can be `NULL` for "never expires"
142
141
 
143
142
  If your table still has a `start_at` column but you want to ignore it, disable it via options:
144
143
 
145
144
  ```ruby
146
145
  class Event < ActiveRecord::Base
147
- include InTimeScope
148
-
149
146
  # Ignore start_at and only use end_at
150
- in_time_scope start_at: { column: nil }, end_at: { null: true }
147
+ in_time_scope start_at: { column: nil }, end_at: { null: false }
151
148
  end
152
149
 
153
150
  Event.in_time(Time.parse("2024-06-01 12:00:00"))
154
- # => SELECT "events".* FROM "events" WHERE ("events"."end_at" IS NULL OR "events"."end_at" > '2024-06-01 12:00:00.000000') /* loading for pp */ LIMIT $1 [["LIMIT", 11]]
155
- ```
156
-
157
- To fetch active rows:
158
-
159
- ```sql
160
- SELECT *
161
- FROM events
162
- WHERE end_at IS NULL OR end_at > NOW();
151
+ # => SELECT "events".* FROM "events" WHERE "events"."end_at" > '2024-06-01 12:00:00.000000'
163
152
  ```
164
153
 
165
154
  Recommended index:
@@ -175,32 +164,45 @@ Customize which columns are used and define more than one time window per model.
175
164
  create_table :events do |t|
176
165
  t.datetime :available_at, null: true
177
166
  t.datetime :expired_at, null: true
178
- t.datetime :publish_start_at, null: false
179
- t.datetime :publish_end_at, null: false
167
+ t.datetime :published_start_at, null: false
168
+ t.datetime :published_end_at, null: false
180
169
 
181
170
  t.timestamps
182
171
  end
183
172
 
184
173
  class Event < ActiveRecord::Base
185
- include InTimeScope
186
-
187
174
  # Use different column names
188
175
  in_time_scope start_at: { column: :available_at }, end_at: { column: :expired_at }
189
176
 
190
- # Define an additional scope
191
- in_time_scope :published, start_at: { column: :publish_start_at, null: false }, end_at: { column: :publish_end_at, null: false }
177
+ # Define an additional scope - uses published_start_at / published_end_at by default
178
+ in_time_scope :published
192
179
  end
193
180
 
194
181
  Event.in_time
195
182
  # => uses available_at / expired_at
196
183
 
184
+ Event.in_time_published
185
+ # => uses published_start_at / published_end_at
186
+ ```
187
+
188
+ ### Using `prefix: true` Option
189
+ Use the `prefix: true` option if you prefer the scope name as a prefix instead of suffix.
190
+
191
+ ```ruby
192
+ class Event < ActiveRecord::Base
193
+ # With prefix: true, the method name becomes published_in_time instead of in_time_published
194
+ in_time_scope :published, prefix: true
195
+ end
196
+
197
197
  Event.published_in_time
198
- # => uses publish_start_at / publish_end_at
198
+ # => uses published_start_at / published_end_at
199
199
  ```
200
200
 
201
201
  ### Using with `has_one` Associations
202
202
 
203
- The start-only pattern provides two scopes for `has_one` associations:
203
+ The start-only and end-only patterns provide `latest_in_time` and `earliest_in_time` scopes for efficient `has_one` associations.
204
+
205
+ **Note:** These scopes are NOT available for full time window patterns (both `start_at` and `end_at`), because the concept of "latest" or "earliest" is ambiguous when there's a time range.
204
206
 
205
207
  #### Simple approach: `in_time` + `order`
206
208
 
@@ -208,7 +210,6 @@ The start-only pattern provides two scopes for `has_one` associations:
208
210
 
209
211
  ```ruby
210
212
  class Price < ActiveRecord::Base
211
- include InTimeScope
212
213
  belongs_to :user
213
214
 
214
215
  in_time_scope start_at: { null: false }, end_at: { column: nil }
@@ -250,6 +251,44 @@ end
250
251
 
251
252
  The `latest_in_time(:foreign_key)` scope uses a `NOT EXISTS` subquery to filter at the database level, avoiding loading unnecessary records into memory.
252
253
 
254
+ #### Getting the earliest record: `earliest_in_time`
255
+
256
+ ```ruby
257
+ class User < ActiveRecord::Base
258
+ has_many :prices
259
+
260
+ # Uses NOT EXISTS subquery - only loads the earliest record per user
261
+ has_one :first_price,
262
+ -> { earliest_in_time(:user_id) },
263
+ class_name: "Price"
264
+ end
265
+
266
+ # Direct access
267
+ user.first_price
268
+ # => Returns the earliest price where start_at <= Time.current
269
+
270
+ # Efficient with includes
271
+ User.includes(:first_price).each do |user|
272
+ puts user.first_price&.amount
273
+ end
274
+ ```
275
+
276
+ The `earliest_in_time(:foreign_key)` scope uses a `NOT EXISTS` subquery to find records where no earlier record exists for the same foreign key.
277
+
278
+ ### Error Handling
279
+
280
+ If you specify a scope name but the expected columns don't exist, a `ColumnNotFoundError` is raised at class load time:
281
+
282
+ ```ruby
283
+ class Event < ActiveRecord::Base
284
+ # This will raise ColumnNotFoundError if hoge_start_at or hoge_end_at columns don't exist
285
+ in_time_scope :hoge
286
+ end
287
+ # => InTimeScope::ColumnNotFoundError: Column 'hoge_start_at' does not exist on table 'events'
288
+ ```
289
+
290
+ This helps catch configuration errors early during development.
291
+
253
292
  ## Development
254
293
 
255
294
  After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/Steepfile ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Steepfile for InTimeScope type checking
4
+
5
+ target :lib do
6
+ signature "sig"
7
+
8
+ check "lib"
9
+
10
+ # Use RBS collection for external gem types
11
+ collection_config "rbs_collection.yaml"
12
+
13
+ # Configure libraries
14
+ library "time"
15
+
16
+ # Ignore implementation details that use ActiveRecord internals
17
+ # The public API is properly typed, but internal methods use
18
+ # dynamic ActiveRecord features that are hard to type statically
19
+ configure_code_diagnostics do |hash|
20
+ # Allow untyped method calls for ActiveRecord dynamic methods
21
+ hash[Steep::Diagnostic::Ruby::NoMethod] = :hint
22
+ hash[Steep::Diagnostic::Ruby::UnknownInstanceVariable] = :hint
23
+ hash[Steep::Diagnostic::Ruby::RequiredBlockMissing] = :hint
24
+ end
25
+ end
@@ -0,0 +1,345 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InTimeScope
4
+ # Class methods added to ActiveRecord models when InTimeScope is included
5
+ module ClassMethods
6
+ # Defines time-window scopes for the model.
7
+ #
8
+ # This method creates both a class-level scope and an instance method
9
+ # to check if records fall within a specified time window.
10
+ #
11
+ # @param scope_name [Symbol] The name of the scope (default: :in_time)
12
+ # When not :in_time, columns default to +<scope_name>_start_at+ and +<scope_name>_end_at+
13
+ #
14
+ # @param start_at [Hash] Configuration for the start column
15
+ # @option start_at [Symbol, nil] :column Column name (nil to disable start boundary)
16
+ # @option start_at [Boolean] :null Whether the column allows NULL values
17
+ # (auto-detected from schema if not specified)
18
+ #
19
+ # @param end_at [Hash] Configuration for the end column
20
+ # @option end_at [Symbol, nil] :column Column name (nil to disable end boundary)
21
+ # @option end_at [Boolean] :null Whether the column allows NULL values
22
+ # (auto-detected from schema if not specified)
23
+ #
24
+ # @param prefix [Boolean] If true, creates +<scope_name>_in_time+ instead of +in_time_<scope_name>+
25
+ #
26
+ # @raise [ColumnNotFoundError] When a specified column doesn't exist (at class load time)
27
+ # @raise [ConfigurationError] When both columns are nil, or when using start-only/end-only
28
+ # pattern with a nullable column (at scope call time)
29
+ #
30
+ # @return [void]
31
+ #
32
+ # == Examples
33
+ #
34
+ # Default scope with nullable columns:
35
+ #
36
+ # in_time_scope
37
+ # # Creates: Model.in_time, model.in_time?
38
+ #
39
+ # Named scope:
40
+ #
41
+ # in_time_scope :published
42
+ # # Creates: Model.in_time_published, model.in_time_published?
43
+ # # Uses: published_start_at, published_end_at columns
44
+ #
45
+ # Custom columns:
46
+ #
47
+ # in_time_scope start_at: { column: :available_at }, end_at: { column: :expired_at }
48
+ #
49
+ # Start-only pattern (for history tracking):
50
+ #
51
+ # in_time_scope start_at: { null: false }, end_at: { column: nil }
52
+ # # Also creates: Model.latest_in_time(:foreign_key), Model.earliest_in_time(:foreign_key)
53
+ #
54
+ # End-only pattern (for expiration):
55
+ #
56
+ # in_time_scope start_at: { column: nil }, end_at: { null: false }
57
+ # # Also creates: Model.latest_in_time(:foreign_key), Model.earliest_in_time(:foreign_key)
58
+ #
59
+ def in_time_scope(scope_name = :in_time, start_at: {}, end_at: {}, prefix: false)
60
+ table_column_hash = columns_hash
61
+ time_column_prefix = scope_name == :in_time ? "" : "#{scope_name}_"
62
+
63
+ start_at_column = start_at.fetch(:column, :"#{time_column_prefix}start_at")
64
+ end_at_column = end_at.fetch(:column, :"#{time_column_prefix}end_at")
65
+
66
+ start_at_null = fetch_null_option(start_at, start_at_column, table_column_hash)
67
+ end_at_null = fetch_null_option(end_at, end_at_column, table_column_hash)
68
+
69
+ scope_method_name = method_name(scope_name, prefix)
70
+
71
+ define_scope_methods(
72
+ scope_method_name,
73
+ start_at_column: start_at_column,
74
+ start_at_null: start_at_null,
75
+ end_at_column: end_at_column,
76
+ end_at_null: end_at_null
77
+ )
78
+ end
79
+
80
+ private
81
+
82
+ # Fetches the null option for a column, auto-detecting from schema if not specified
83
+ #
84
+ # @param config [Hash] Configuration hash with optional :null key
85
+ # @param column [Symbol, nil] Column name
86
+ # @param table_column_hash [Hash] Hash of column metadata from ActiveRecord
87
+ # @return [Boolean, nil] Whether the column allows NULL values
88
+ # @raise [ColumnNotFoundError] When the column doesn't exist in the table
89
+ # @api private
90
+ def fetch_null_option(config, column, table_column_hash)
91
+ return nil if column.nil?
92
+ return config[:null] if config.key?(:null)
93
+
94
+ column_info = table_column_hash[column.to_s]
95
+ raise ColumnNotFoundError, "Column '#{column}' does not exist on table '#{table_name}'" if column_info.nil?
96
+
97
+ column_info.null
98
+ end
99
+
100
+ # Generates the method name for the scope
101
+ #
102
+ # @param scope_name [Symbol] The scope name
103
+ # @param prefix [Boolean] Whether to use prefix style
104
+ # @return [Symbol] The generated method name
105
+ # @api private
106
+ def method_name(scope_name, prefix)
107
+ return :in_time if scope_name == :in_time
108
+
109
+ prefix ? "#{scope_name}_in_time" : "in_time_#{scope_name}"
110
+ end
111
+
112
+ # Defines the appropriate scope methods based on configuration
113
+ #
114
+ # @param scope_method_name [Symbol] The name of the scope method to create
115
+ # @param start_at_column [Symbol, nil] Start column name
116
+ # @param start_at_null [Boolean, nil] Whether start column allows NULL
117
+ # @param end_at_column [Symbol, nil] End column name
118
+ # @param end_at_null [Boolean, nil] Whether end column allows NULL
119
+ # @return [void]
120
+ # @api private
121
+ def define_scope_methods(scope_method_name, start_at_column:, start_at_null:, end_at_column:, end_at_null:)
122
+ # Define class-level scope and instance method
123
+ if start_at_column.nil? && end_at_column.nil?
124
+ define_error_scope_and_method(scope_method_name,
125
+ "At least one of start_at or end_at must be specified")
126
+ elsif end_at_column.nil?
127
+ # Start-only pattern (history tracking) - requires non-nullable column
128
+ if start_at_null
129
+ define_error_scope_and_method(scope_method_name,
130
+ "Start-only pattern requires non-nullable column. " \
131
+ "Set `start_at: { null: false }` or add an end_at column")
132
+ else
133
+ define_start_only_scope(scope_method_name, start_at_column)
134
+ define_instance_method(scope_method_name, start_at_column, start_at_null, end_at_column, end_at_null)
135
+ end
136
+ elsif start_at_column.nil?
137
+ # End-only pattern (expiration) - requires non-nullable column
138
+ if end_at_null
139
+ define_error_scope_and_method(scope_method_name,
140
+ "End-only pattern requires non-nullable column. " \
141
+ "Set `end_at: { null: false }` or add a start_at column")
142
+ else
143
+ define_end_only_scope(scope_method_name, end_at_column)
144
+ define_instance_method(scope_method_name, start_at_column, start_at_null, end_at_column, end_at_null)
145
+ end
146
+ else
147
+ # Both start and end
148
+ define_full_scope(scope_method_name, start_at_column, start_at_null, end_at_column, end_at_null)
149
+ define_instance_method(scope_method_name, start_at_column, start_at_null, end_at_column, end_at_null)
150
+ end
151
+ end
152
+
153
+ # Defines a scope and instance method that raise ConfigurationError
154
+ #
155
+ # @param scope_method_name [Symbol] The name of the scope method
156
+ # @param message [String] The error message
157
+ # @return [void]
158
+ # @api private
159
+ def define_error_scope_and_method(scope_method_name, message)
160
+ err_message = message
161
+
162
+ scope scope_method_name, ->(_time = Time.current) {
163
+ raise InTimeScope::ConfigurationError, err_message
164
+ }
165
+
166
+ define_method("#{scope_method_name}?") do |_time = Time.current|
167
+ raise InTimeScope::ConfigurationError, err_message
168
+ end
169
+ end
170
+
171
+ # Defines a start-only scope (for history tracking pattern)
172
+ #
173
+ # @param scope_method_name [Symbol] The name of the scope method
174
+ # @param column [Symbol] The start column name
175
+ # @return [void]
176
+ # @api private
177
+ def define_start_only_scope(scope_method_name, column)
178
+ col = column
179
+
180
+ # Simple scope - WHERE only, no ORDER BY
181
+ # Users can add .order(start_at: :desc) externally if needed
182
+ scope scope_method_name, ->(time = Time.current) {
183
+ where(col => ..time)
184
+ }
185
+
186
+ # Efficient scope for has_one + includes using NOT EXISTS subquery
187
+ # Usage: has_one :current_price, -> { latest_in_time(:user_id) }, class_name: 'Price'
188
+ define_latest_one_scope(scope_method_name, column)
189
+ define_earliest_one_scope(scope_method_name, column)
190
+ end
191
+
192
+ # Defines the latest_in_time scope using NOT EXISTS subquery
193
+ #
194
+ # This scope efficiently finds the latest record per foreign key,
195
+ # suitable for use with has_one associations and includes.
196
+ #
197
+ # @param scope_method_name [Symbol] The base scope method name
198
+ # @param column [Symbol] The timestamp column name
199
+ # @return [void]
200
+ #
201
+ # @example Usage with has_one
202
+ # has_one :current_price, -> { latest_in_time(:user_id) }, class_name: 'Price'
203
+ #
204
+ # @api private
205
+ def define_latest_one_scope(scope_method_name, column)
206
+ latest_method_name = scope_method_name == :in_time ? :latest_in_time : :"latest_#{scope_method_name}"
207
+ col = column
208
+
209
+ # NOT EXISTS approach: select records where no later record exists for the same foreign key
210
+ scope latest_method_name, ->(foreign_key, time = Time.current) {
211
+ p2 = arel_table.alias("p2")
212
+
213
+ subquery = Arel::SelectManager.new(arel_table)
214
+ .from(p2)
215
+ .project(Arel.sql("1"))
216
+ .where(p2[foreign_key].eq(arel_table[foreign_key]))
217
+ .where(p2[col].lteq(time))
218
+ .where(p2[col].gt(arel_table[col]))
219
+ .where(p2[:id].not_eq(arel_table[:id]))
220
+
221
+ not_exists = Arel::Nodes::Not.new(Arel::Nodes::Exists.new(subquery.ast))
222
+
223
+ where(col => ..time).where(not_exists)
224
+ }
225
+ end
226
+
227
+ # Defines the earliest_in_time scope using NOT EXISTS subquery
228
+ #
229
+ # This scope efficiently finds the earliest record per foreign key,
230
+ # suitable for use with has_one associations and includes.
231
+ #
232
+ # @param scope_method_name [Symbol] The base scope method name
233
+ # @param column [Symbol] The timestamp column name
234
+ # @return [void]
235
+ #
236
+ # @example Usage with has_one
237
+ # has_one :first_price, -> { earliest_in_time(:user_id) }, class_name: 'Price'
238
+ #
239
+ # @api private
240
+ def define_earliest_one_scope(scope_method_name, column)
241
+ earliest_method_name = scope_method_name == :in_time ? :earliest_in_time : :"earliest_#{scope_method_name}"
242
+ col = column
243
+
244
+ # NOT EXISTS approach: select records where no earlier record exists for the same foreign key
245
+ scope earliest_method_name, ->(foreign_key, time = Time.current) {
246
+ p2 = arel_table.alias("p2")
247
+
248
+ subquery = Arel::SelectManager.new(arel_table)
249
+ .from(p2)
250
+ .project(Arel.sql("1"))
251
+ .where(p2[foreign_key].eq(arel_table[foreign_key]))
252
+ .where(p2[col].lteq(time))
253
+ .where(p2[col].lt(arel_table[col]))
254
+ .where(p2[:id].not_eq(arel_table[:id]))
255
+
256
+ not_exists = Arel::Nodes::Not.new(Arel::Nodes::Exists.new(subquery.ast))
257
+
258
+ where(col => ..time).where(not_exists)
259
+ }
260
+ end
261
+
262
+ # Defines an end-only scope (for expiration pattern)
263
+ #
264
+ # @param scope_method_name [Symbol] The name of the scope method
265
+ # @param column [Symbol] The end column name
266
+ # @return [void]
267
+ # @api private
268
+ def define_end_only_scope(scope_method_name, column)
269
+ col = column
270
+
271
+ scope scope_method_name, ->(time = Time.current) {
272
+ where.not(col => ..time)
273
+ }
274
+
275
+ # Efficient scope for has_one + includes using NOT EXISTS subquery
276
+ define_latest_one_scope(scope_method_name, column)
277
+ define_earliest_one_scope(scope_method_name, column)
278
+ end
279
+
280
+ # Defines a full scope with both start and end columns
281
+ #
282
+ # @param scope_method_name [Symbol] The name of the scope method
283
+ # @param start_column [Symbol] The start column name
284
+ # @param start_null [Boolean] Whether start column allows NULL
285
+ # @param end_column [Symbol] The end column name
286
+ # @param end_null [Boolean] Whether end column allows NULL
287
+ # @return [void]
288
+ # @api private
289
+ def define_full_scope(scope_method_name, start_column, start_null, end_column, end_null)
290
+ s_col = start_column
291
+ e_col = end_column
292
+
293
+ scope scope_method_name, ->(time = Time.current) {
294
+ start_scope = if start_null
295
+ where(s_col => nil).or(where(s_col => ..time))
296
+ else
297
+ where(s_col => ..time)
298
+ end
299
+
300
+ end_scope = if end_null
301
+ where(e_col => nil).or(where.not(e_col => ..time))
302
+ else
303
+ where.not(e_col => ..time)
304
+ end
305
+
306
+ start_scope.merge(end_scope)
307
+ }
308
+
309
+ # NOTE: latest_in_time / earliest_in_time are NOT defined for full scope (both start and end)
310
+ # because the concept of "latest" or "earliest" is ambiguous when there's a time range.
311
+ # These scopes are only available for start-only or end-only patterns.
312
+ end
313
+
314
+ # Defines the instance method to check if a record is within the time window
315
+ #
316
+ # @param scope_method_name [Symbol] The name of the scope method
317
+ # @param start_column [Symbol, nil] The start column name
318
+ # @param start_null [Boolean, nil] Whether start column allows NULL
319
+ # @param end_column [Symbol, nil] The end column name
320
+ # @param end_null [Boolean, nil] Whether end column allows NULL
321
+ # @return [void]
322
+ # @api private
323
+ def define_instance_method(scope_method_name, start_column, start_null, end_column, end_null)
324
+ define_method("#{scope_method_name}?") do |time = Time.current|
325
+ start_ok = if start_column.nil?
326
+ true
327
+ elsif start_null
328
+ send(start_column).nil? || send(start_column) <= time
329
+ else
330
+ send(start_column) <= time
331
+ end
332
+
333
+ end_ok = if end_column.nil?
334
+ true
335
+ elsif end_null
336
+ send(end_column).nil? || send(end_column) > time
337
+ else
338
+ send(end_column) > time
339
+ end
340
+
341
+ start_ok && end_ok
342
+ end
343
+ end
344
+ end
345
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module InTimeScope
4
- VERSION = "0.1.0"
4
+ # The current version of the InTimeScope gem
5
+ VERSION = "0.1.5"
5
6
  end
data/lib/in_time_scope.rb CHANGED
@@ -2,187 +2,128 @@
2
2
 
3
3
  require "active_record"
4
4
  require_relative "in_time_scope/version"
5
-
5
+ require_relative "in_time_scope/class_methods"
6
+
7
+ # InTimeScope provides time-window scopes for ActiveRecord models.
8
+ #
9
+ # It allows you to easily query records that fall within specific time periods,
10
+ # with support for nullable columns, custom column names, and multiple scopes per model.
11
+ #
12
+ # InTimeScope is automatically included in ActiveRecord::Base, so you can use
13
+ # +in_time_scope+ directly in your models without explicit include.
14
+ #
15
+ # == Basic usage
16
+ #
17
+ # class Event < ActiveRecord::Base
18
+ # in_time_scope
19
+ # end
20
+ #
21
+ # Event.in_time # Records active at current time
22
+ # Event.in_time(some_time) # Records active at specific time
23
+ # event.in_time? # Check if record is active now
24
+ #
25
+ # == Patterns
26
+ #
27
+ # === Full pattern (both start and end)
28
+ #
29
+ # Default pattern with both +start_at+ and +end_at+ columns.
30
+ # Supports nullable columns (NULL means "no limit").
31
+ #
32
+ # class Event < ActiveRecord::Base
33
+ # in_time_scope # Uses start_at and end_at columns
34
+ # end
35
+ #
36
+ # === Start-only pattern (history tracking)
37
+ #
38
+ # For versioned records where each row is valid from +start_at+ until the next row.
39
+ # Requires non-nullable column.
40
+ #
41
+ # class Price < ActiveRecord::Base
42
+ # in_time_scope start_at: { null: false }, end_at: { column: nil }
43
+ # end
44
+ #
45
+ # # Additional scopes created:
46
+ # Price.latest_in_time(:user_id) # Latest record per user
47
+ # Price.earliest_in_time(:user_id) # Earliest record per user
48
+ #
49
+ # === End-only pattern (expiration)
50
+ #
51
+ # For records that are always active until they expire.
52
+ # Requires non-nullable column.
53
+ #
54
+ # class Coupon < ActiveRecord::Base
55
+ # in_time_scope start_at: { column: nil }, end_at: { null: false }
56
+ # end
57
+ #
58
+ # == Using with has_one associations
59
+ #
60
+ # The +latest_in_time+ and +earliest_in_time+ scopes are optimized for
61
+ # +has_one+ associations with +includes+, using NOT EXISTS subqueries.
62
+ #
63
+ # class Price < ActiveRecord::Base
64
+ # belongs_to :user
65
+ # in_time_scope start_at: { null: false }, end_at: { column: nil }
66
+ # end
67
+ #
68
+ # class User < ActiveRecord::Base
69
+ # has_many :prices
70
+ #
71
+ # # Efficient: uses NOT EXISTS subquery
72
+ # has_one :current_price,
73
+ # -> { latest_in_time(:user_id) },
74
+ # class_name: "Price"
75
+ #
76
+ # has_one :first_price,
77
+ # -> { earliest_in_time(:user_id) },
78
+ # class_name: "Price"
79
+ # end
80
+ #
81
+ # # Works efficiently with includes
82
+ # User.includes(:current_price).each do |user|
83
+ # puts user.current_price&.amount
84
+ # end
85
+ #
86
+ # == Named scopes
87
+ #
88
+ # Define multiple time windows per model using named scopes.
89
+ #
90
+ # class Article < ActiveRecord::Base
91
+ # in_time_scope :published # Uses published_start_at, published_end_at
92
+ # in_time_scope :featured # Uses featured_start_at, featured_end_at
93
+ # end
94
+ #
95
+ # Article.in_time_published
96
+ # Article.in_time_featured
97
+ # article.in_time_published?
98
+ #
99
+ # == Custom columns
100
+ #
101
+ # class Event < ActiveRecord::Base
102
+ # in_time_scope start_at: { column: :available_at },
103
+ # end_at: { column: :expired_at }
104
+ # end
105
+ #
106
+ # == Error handling
107
+ #
108
+ # - ColumnNotFoundError: Raised at class load time if column doesn't exist
109
+ # - ConfigurationError: Raised at scope call time for invalid configurations
110
+ #
111
+ # @see ClassMethods#in_time_scope
6
112
  module InTimeScope
113
+ # Base error class for InTimeScope errors
7
114
  class Error < StandardError; end
8
115
 
9
- def self.included(model)
10
- model.extend ClassMethods
11
- end
12
-
13
- module ClassMethods
14
- def in_time_scope(scope_name = nil, start_at: {}, end_at: {})
15
- scope_name ||= :in_time
16
- scope_suffix = scope_name == :in_time ? "" : "_#{scope_name}"
17
-
18
- start_config = normalize_config(start_at, :"start_at#{scope_suffix}", :start_at)
19
- end_config = normalize_config(end_at, :"end_at#{scope_suffix}", :end_at)
20
-
21
- define_scope_methods(scope_name, start_config, end_config)
22
- end
23
-
24
- private
25
-
26
- def normalize_config(config, default_column, fallback_column)
27
- return { column: nil, null: true } if config[:column].nil? && config.key?(:column)
28
-
29
- column = config[:column] || default_column
30
- column = fallback_column unless column_names.include?(column.to_s)
31
- column = nil unless column_names.include?(column.to_s)
32
-
33
- null = config.key?(:null) ? config[:null] : column_nullable?(column)
34
-
35
- { column: column, null: null }
36
- end
37
-
38
- def column_nullable?(column_name)
39
- return true if column_name.nil?
40
-
41
- col = columns_hash[column_name.to_s]
42
- col ? col.null : true
43
- end
44
-
45
- def define_scope_methods(scope_name, start_config, end_config)
46
- method_name = scope_name == :in_time ? :in_time : :"#{scope_name}_in_time"
47
- instance_method_name = :"#{method_name}?"
48
-
49
- start_column = start_config[:column]
50
- start_null = start_config[:null]
51
- end_column = end_config[:column]
52
- end_null = end_config[:null]
116
+ # Raised when a specified column does not exist on the table
117
+ # @note This error is raised at class load time
118
+ class ColumnNotFoundError < Error; end
53
119
 
54
- # Define class-level scope
55
- if start_column.nil? && end_column.nil?
56
- # Both disabled - return all
57
- scope method_name, ->(_time = Time.current) { all }
58
- elsif end_column.nil?
59
- # Start-only pattern (history tracking)
60
- define_start_only_scope(method_name, start_column, start_null)
61
- elsif start_column.nil?
62
- # End-only pattern (expiration)
63
- define_end_only_scope(method_name, end_column, end_null)
64
- else
65
- # Both start and end
66
- define_full_scope(method_name, start_column, start_null, end_column, end_null)
67
- end
120
+ # Raised when the scope configuration is invalid
121
+ # @note This error is raised when the scope or instance method is called
122
+ class ConfigurationError < Error; end
68
123
 
69
- # Define instance method
70
- define_instance_method(instance_method_name, start_column, start_null, end_column, end_null)
71
- end
72
-
73
- def define_start_only_scope(method_name, start_column, start_null)
74
- # Simple scope - WHERE only, no ORDER BY
75
- # Users can add .order(start_at: :desc) externally if needed
76
- if start_null
77
- scope method_name, ->(time = Time.current) {
78
- where(arel_table[start_column].eq(nil).or(arel_table[start_column].lteq(time)))
79
- }
80
- else
81
- scope method_name, ->(time = Time.current) {
82
- where(arel_table[start_column].lteq(time))
83
- }
84
- end
85
-
86
- # Efficient scope for has_one + includes using NOT EXISTS subquery
87
- # Usage: has_one :current_price, -> { latest_in_time(:user_id) }, class_name: 'Price'
88
- define_latest_scope(method_name, start_column, start_null)
89
- end
90
-
91
- def define_latest_scope(method_name, start_column, start_null)
92
- latest_method_name = method_name == :in_time ? :latest_in_time : :"latest_#{method_name}_in_time"
93
- tbl = table_name
94
- col = start_column
95
-
96
- # NOT EXISTS approach: select records where no later record exists for the same foreign key
97
- # SELECT * FROM prices p1 WHERE start_at <= ? AND NOT EXISTS (
98
- # SELECT 1 FROM prices p2 WHERE p2.user_id = p1.user_id
99
- # AND p2.start_at <= ? AND p2.start_at > p1.start_at
100
- # )
101
- scope latest_method_name, ->(foreign_key, time = Time.current) {
102
- fk = foreign_key
103
-
104
- not_exists_sql = if start_null
105
- <<~SQL.squish
106
- NOT EXISTS (
107
- SELECT 1 FROM #{tbl} p2
108
- WHERE p2.#{fk} = #{tbl}.#{fk}
109
- AND (p2.#{col} IS NULL OR p2.#{col} <= ?)
110
- AND (p2.#{col} IS NULL OR p2.#{col} > #{tbl}.#{col} OR #{tbl}.#{col} IS NULL)
111
- AND p2.id != #{tbl}.id
112
- )
113
- SQL
114
- else
115
- <<~SQL.squish
116
- NOT EXISTS (
117
- SELECT 1 FROM #{tbl} p2
118
- WHERE p2.#{fk} = #{tbl}.#{fk}
119
- AND p2.#{col} <= ?
120
- AND p2.#{col} > #{tbl}.#{col}
121
- )
122
- SQL
123
- end
124
-
125
- base_condition = if start_null
126
- where(arel_table[col].eq(nil).or(arel_table[col].lteq(time)))
127
- else
128
- where(arel_table[col].lteq(time))
129
- end
130
-
131
- base_condition.where(not_exists_sql, time)
132
- }
133
- end
134
-
135
- def define_end_only_scope(method_name, end_column, end_null)
136
- if end_null
137
- scope method_name, ->(time = Time.current) {
138
- where(arel_table[end_column].eq(nil).or(arel_table[end_column].gt(time)))
139
- }
140
- else
141
- scope method_name, ->(time = Time.current) {
142
- where(arel_table[end_column].gt(time))
143
- }
144
- end
145
- end
146
-
147
- def define_full_scope(method_name, start_column, start_null, end_column, end_null)
148
- scope method_name, ->(time = Time.current) {
149
- start_condition = if start_null
150
- arel_table[start_column].eq(nil).or(arel_table[start_column].lteq(time))
151
- else
152
- arel_table[start_column].lteq(time)
153
- end
154
-
155
- end_condition = if end_null
156
- arel_table[end_column].eq(nil).or(arel_table[end_column].gt(time))
157
- else
158
- arel_table[end_column].gt(time)
159
- end
160
-
161
- where(start_condition).where(end_condition)
162
- }
163
- end
164
-
165
- def define_instance_method(method_name, start_column, start_null, end_column, end_null)
166
- define_method(method_name) do |time = Time.current|
167
- start_ok = if start_column.nil?
168
- true
169
- elsif start_null
170
- send(start_column).nil? || send(start_column) <= time
171
- else
172
- send(start_column) <= time
173
- end
174
-
175
- end_ok = if end_column.nil?
176
- true
177
- elsif end_null
178
- send(end_column).nil? || send(end_column) > time
179
- else
180
- send(end_column) > time
181
- end
182
-
183
- start_ok && end_ok
184
- end
185
- end
124
+ # @api private
125
+ def self.included(model)
126
+ model.extend ClassMethods
186
127
  end
187
128
  end
188
129
 
@@ -0,0 +1,16 @@
1
+ # RBS collection configuration
2
+ # Run `rbs collection install` to download external gem signatures
3
+
4
+ sources:
5
+ - type: git
6
+ name: ruby/gem_rbs_collection
7
+ remote: https://github.com/ruby/gem_rbs_collection.git
8
+ revision: main
9
+ repo_dir: gems
10
+
11
+ path: .gem_rbs_collection
12
+
13
+ gems:
14
+ - name: activerecord
15
+ - name: activesupport
16
+ - name: activemodel
@@ -1,4 +1,83 @@
1
+ # Type definitions for InTimeScope gem
2
+
1
3
  module InTimeScope
2
4
  VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+
6
+ # Base error class for InTimeScope errors
7
+ class Error < StandardError
8
+ end
9
+
10
+ # Raised when a specified column does not exist on the table
11
+ class ColumnNotFoundError < Error
12
+ end
13
+
14
+ # Raised when the scope configuration is invalid
15
+ class ConfigurationError < Error
16
+ end
17
+
18
+ def self.included: (Class model) -> void
19
+
20
+ # Configuration options for start_at column
21
+ type start_at_config = {
22
+ ?column: Symbol?,
23
+ ?null: bool
24
+ }
25
+
26
+ # Configuration options for end_at column
27
+ type end_at_config = {
28
+ ?column: Symbol?,
29
+ ?null: bool
30
+ }
31
+
32
+ # Class methods added to ActiveRecord models when InTimeScope is included
33
+ module ClassMethods
34
+ # Defines time-window scopes for the model
35
+ #
36
+ # @param scope_name [Symbol] The name of the scope (default: :in_time)
37
+ # @param start_at [Hash] Configuration for the start column
38
+ # @param end_at [Hash] Configuration for the end column
39
+ # @param prefix [Boolean] If true, creates prefix-style method names
40
+ # @return [void]
41
+ # @raise [ColumnNotFoundError] When a specified column doesn't exist
42
+ # @raise [ConfigurationError] When the configuration is invalid (at scope call time)
43
+ def in_time_scope: (
44
+ ?Symbol scope_name,
45
+ ?start_at: start_at_config,
46
+ ?end_at: end_at_config,
47
+ ?prefix: bool
48
+ ) -> void
49
+
50
+ private
51
+
52
+ # Private implementation methods
53
+ # These use ActiveRecord internals and are typed as untyped for flexibility
54
+ def fetch_null_option: (untyped config, untyped column, untyped table_column_hash) -> untyped
55
+ def method_name: (Symbol scope_name, bool prefix) -> (Symbol | String)
56
+ def define_scope_methods: (untyped scope_method_name, start_at_column: untyped, start_at_null: untyped, end_at_column: untyped, end_at_null: untyped) -> void
57
+ def define_error_scope_and_method: (untyped scope_method_name, String message) -> void
58
+ def define_start_only_scope: (untyped scope_method_name, Symbol column) -> void
59
+ def define_latest_one_scope: (untyped scope_method_name, Symbol column) -> void
60
+ def define_earliest_one_scope: (untyped scope_method_name, Symbol column) -> void
61
+ def define_end_only_scope: (untyped scope_method_name, Symbol column) -> void
62
+ def define_full_scope: (untyped scope_method_name, Symbol start_column, untyped start_null, Symbol end_column, untyped end_null) -> void
63
+ def define_instance_method: (untyped scope_method_name, Symbol? start_column, untyped start_null, Symbol? end_column, untyped end_null) -> void
64
+ end
65
+ end
66
+
67
+ # Generated scope methods (dynamically defined)
68
+ # When you call `in_time_scope` on a model, it creates these methods:
69
+ #
70
+ # Class methods:
71
+ # Model.in_time(time = Time.current) -> ActiveRecord::Relation
72
+ # Model.in_time_<name>(time = Time.current) -> ActiveRecord::Relation (for named scopes)
73
+ # Model.latest_in_time(foreign_key, time = Time.current) -> ActiveRecord::Relation (start-only/end-only)
74
+ # Model.earliest_in_time(foreign_key, time = Time.current) -> ActiveRecord::Relation (start-only/end-only)
75
+ #
76
+ # Instance methods:
77
+ # model.in_time?(time = Time.current) -> bool
78
+ # model.in_time_<name>?(time = Time.current) -> bool (for named scopes)
79
+
80
+ # Extend ActiveRecord::Base to include InTimeScope
81
+ class ActiveRecord::Base
82
+ extend InTimeScope::ClassMethods
4
83
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: in_time_scope
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - kyohah
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-01-24 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activerecord
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '6.0'
18
+ version: '0'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '6.0'
25
+ version: '0'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: irb
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '13.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rbs
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
54
68
  - !ruby/object:Gem::Dependency
55
69
  name: rspec
56
70
  requirement: !ruby/object:Gem::Requirement
@@ -93,6 +107,34 @@ dependencies:
93
107
  - - ">="
94
108
  - !ruby/object:Gem::Version
95
109
  version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: steep
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ - !ruby/object:Gem::Dependency
125
+ name: yard
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
96
138
  description: InTimeScope provides time-window scopes for ActiveRecord models.
97
139
  email:
98
140
  - 3257272+kyohah@users.noreply.github.com
@@ -107,8 +149,11 @@ files:
107
149
  - LICENSE.txt
108
150
  - README.md
109
151
  - Rakefile
152
+ - Steepfile
110
153
  - lib/in_time_scope.rb
154
+ - lib/in_time_scope/class_methods.rb
111
155
  - lib/in_time_scope/version.rb
156
+ - rbs_collection.yaml
112
157
  - sig/in_time_scope.rbs
113
158
  homepage: https://github.com/kyohah/in_time_scope
114
159
  licenses:
@@ -126,14 +171,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
126
171
  requirements:
127
172
  - - ">="
128
173
  - !ruby/object:Gem::Version
129
- version: 3.1.0
174
+ version: 3.0.0
130
175
  required_rubygems_version: !ruby/object:Gem::Requirement
131
176
  requirements:
132
177
  - - ">="
133
178
  - !ruby/object:Gem::Version
134
179
  version: '0'
135
180
  requirements: []
136
- rubygems_version: 3.6.2
181
+ rubygems_version: 4.0.3
137
182
  specification_version: 4
138
183
  summary: Add time-window scopes to ActiveRecord models
139
184
  test_files: []