rokaki 0.12.0 → 0.13.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/Gemfile.lock +1 -1
- data/docs/index.md +40 -2
- data/docs/usage.md +90 -2
- data/lib/rokaki/filter_model.rb +84 -1
- data/lib/rokaki/filterable.rb +71 -2
- data/lib/rokaki/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2659a359499939511a45107cc281e2aecce654ecf587f668e9de52d6fd8e3424
|
|
4
|
+
data.tar.gz: 4a5dfa2b6eef83fb731bb558575bdd5072b2c5ff2f4dc3670b11cae9487447ff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a120036a6621d1ca69631d95fccd5fa3fac82cbb4a630bffcc5a5ea87ec026ef416fda109964eec929d1c8874af7a4dc08e06bab2cd1789779332ae9796b645e
|
|
7
|
+
data.tar.gz: 656df3353ae9593cdeaa75ce0b0111728fbbb420d8c4885928fcc334369ceaa07ea09f78a49b93b3ab45ea8d5767b81f9280cd9cf55260690a0a34270947c7ab
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
### 0.13.0 — 2025-10-25
|
|
2
|
+
- Add block-form DSL parity across both FilterModel and Filterable (`filter_map do ... end` with `like`, `ilike`, `nested`, and `filters`).
|
|
3
|
+
- Support circumfix affix synonyms: `:parafix`, `:confix`, `:ambifix` (treated as `:circumfix`).
|
|
4
|
+
- Improve docs with block-form examples and adapter behavior notes.
|
|
5
|
+
- Keep SQL Server support (introduced earlier) covered in CI; add shared tests for affix synonyms to all adapter-aware specs.
|
|
6
|
+
- Allow ENV overrides for all adapters in test DatabaseManager (Postgres/MySQL/SQL Server).
|
|
7
|
+
|
|
8
|
+
### 0.12.0 — 2025-10-25
|
|
9
|
+
- Introduce block-form DSL for FilterModel (`filter_map do ... end`) with `like` and `nested`.
|
|
10
|
+
- Update docs site and GitHub Pages build.
|
|
11
|
+
|
|
12
|
+
### 0.11.0 — 2025-10-25
|
|
13
|
+
- Add first-class SQL Server support.
|
|
14
|
+
- CI updates to run tests against SQL Server, alongside PostgreSQL and MySQL.
|
|
15
|
+
|
|
16
|
+
### 0.10.0 and earlier
|
|
17
|
+
- Core DSL: Filterable and FilterModel modes, LIKE matching with prefix/suffix/circumfix, nested filters, and adapter-aware SQL for Postgres/MySQL.
|
data/Gemfile.lock
CHANGED
data/docs/index.md
CHANGED
|
@@ -21,7 +21,7 @@ Get started below or jump to:
|
|
|
21
21
|
Add to your application's Gemfile:
|
|
22
22
|
|
|
23
23
|
```ruby
|
|
24
|
-
gem "rokaki", "~> 0.
|
|
24
|
+
gem "rokaki", "~> 0.13"
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
Then:
|
|
@@ -32,7 +32,9 @@ bundle install
|
|
|
32
32
|
|
|
33
33
|
## Quick start
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
You can declare mappings in two ways: argument-based (original) or block-form DSL. Both are equivalent.
|
|
36
|
+
|
|
37
|
+
Argument-based form:
|
|
36
38
|
|
|
37
39
|
```ruby
|
|
38
40
|
class ArticleQuery
|
|
@@ -58,6 +60,34 @@ end
|
|
|
58
60
|
filtered = ArticleQuery.new(filters: params).results
|
|
59
61
|
```
|
|
60
62
|
|
|
63
|
+
Block-form DSL (same behavior):
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
class ArticleQuery
|
|
67
|
+
include Rokaki::FilterModel
|
|
68
|
+
|
|
69
|
+
filter_model :article, db: :postgres # or :mysql, :sqlserver
|
|
70
|
+
define_query_key :q
|
|
71
|
+
|
|
72
|
+
filter_map do
|
|
73
|
+
like title: :circumfix, content: :circumfix
|
|
74
|
+
nested :author do
|
|
75
|
+
like first_name: :prefix, last_name: :suffix
|
|
76
|
+
# You can also declare equality filters inside nested contexts
|
|
77
|
+
filters :id
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
attr_accessor :filters
|
|
82
|
+
def initialize(filters: {})
|
|
83
|
+
@filters = filters
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# In a controller/service:
|
|
88
|
+
filtered = ArticleQuery.new(filters: params).results
|
|
89
|
+
```
|
|
90
|
+
|
|
61
91
|
Where `params` can include keys like `q`, `author_first_name`, `author_last_name`, etc. The LIKE mode for each key is defined in your `like` mapping (e.g., `title: :circumfix`), and Rokaki builds the appropriate `WHERE` clauses safely and adapter‑aware.
|
|
62
92
|
|
|
63
93
|
## Matching modes
|
|
@@ -68,6 +98,14 @@ Where `params` can include keys like `q`, `author_first_name`, `author_last_name
|
|
|
68
98
|
|
|
69
99
|
All modes accept either a single string or an array of terms.
|
|
70
100
|
|
|
101
|
+
## What’s new in 0.13.0
|
|
102
|
+
|
|
103
|
+
- Block-form DSL parity across both FilterModel and Filterable
|
|
104
|
+
- Circumfix affix synonyms supported: :parafix, :confix, :ambifix
|
|
105
|
+
- SQL Server adapter support and CI coverage
|
|
106
|
+
- ENV overrides for all adapters in test helpers; improved DB bootstrap in specs
|
|
107
|
+
- Documentation site via GitHub Pages
|
|
108
|
+
|
|
71
109
|
## Next steps
|
|
72
110
|
|
|
73
111
|
- Learn the full DSL and examples in [Usage](./usage)
|
data/docs/usage.md
CHANGED
|
@@ -11,7 +11,7 @@ This page shows how to use Rokaki to define filters and apply them to ActiveReco
|
|
|
11
11
|
Add the gem to your Gemfile and bundle:
|
|
12
12
|
|
|
13
13
|
```ruby
|
|
14
|
-
gem "rokaki", "~> 0.
|
|
14
|
+
gem "rokaki", "~> 0.13"
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
```bash
|
|
@@ -38,7 +38,7 @@ class ArticleQuery
|
|
|
38
38
|
define_query_key :q
|
|
39
39
|
like title: :circumfix, content: :circumfix
|
|
40
40
|
|
|
41
|
-
# Nested LIKEs via hash mapping
|
|
41
|
+
# Nested LIKEs via hash mapping
|
|
42
42
|
like author: { first_name: :prefix, last_name: :suffix }
|
|
43
43
|
end
|
|
44
44
|
```
|
|
@@ -98,3 +98,91 @@ Params would include `author_first`, `author_first_prefix`, etc.
|
|
|
98
98
|
- Use `key:` to map a filter to a different params key.
|
|
99
99
|
- Combine multiple filters; Rokaki composes them with `AND` by default.
|
|
100
100
|
- For advanced cases, write custom filters in your app by extending the DSL (see source for `BasicFilter`/`NestedFilter`).
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
## Block-form DSL
|
|
104
|
+
|
|
105
|
+
Note: The block-form DSL is available starting in Rokaki 0.13.0.
|
|
106
|
+
|
|
107
|
+
Rokaki also supports a block-form DSL that is equivalent to the argument-based form. Use it when you prefer grouping your mappings in a single block.
|
|
108
|
+
|
|
109
|
+
### FilterModel block form
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
class ArticleQuery
|
|
113
|
+
include Rokaki::FilterModel
|
|
114
|
+
|
|
115
|
+
# Choose model and adapter
|
|
116
|
+
filter_model :article, db: :postgres # or :mysql, :sqlserver
|
|
117
|
+
|
|
118
|
+
# Declare a single query key used by all LIKE/equality filters below
|
|
119
|
+
define_query_key :q
|
|
120
|
+
|
|
121
|
+
# Declare mappings inside a block
|
|
122
|
+
filter_map do
|
|
123
|
+
# LIKE mappings on the base model
|
|
124
|
+
like title: :circumfix, content: :circumfix
|
|
125
|
+
|
|
126
|
+
# Nested mappings on associations
|
|
127
|
+
nested :author do
|
|
128
|
+
like first_name: :prefix, last_name: :suffix
|
|
129
|
+
|
|
130
|
+
# You can also declare equality filters in block form
|
|
131
|
+
filters :id
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
attr_accessor :filters
|
|
136
|
+
def initialize(filters: {})
|
|
137
|
+
@filters = filters
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Usage
|
|
142
|
+
ArticleQuery.new(filters: { q: ["Intro", "Guide"] }).results
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Notes:
|
|
146
|
+
- Modes are declared by the values in your `like` mapping (`:prefix`, `:suffix`, `:circumfix`). Synonyms `:parafix`, `:confix`, `:ambifix` behave like `:circumfix`.
|
|
147
|
+
- Arrays for `q` are supported across adapters. PostgreSQL uses `ANY (ARRAY[...])`, MySQL/SQL Server expand to OR chains as appropriate.
|
|
148
|
+
|
|
149
|
+
### Filterable block form
|
|
150
|
+
|
|
151
|
+
Use the block form to define simple key accessors (no SQL). Useful for plain Ruby objects or when building a mapping layer.
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
class ArticleFilters
|
|
155
|
+
include Rokaki::Filterable
|
|
156
|
+
filter_key_prefix :__
|
|
157
|
+
|
|
158
|
+
filter_map do
|
|
159
|
+
filters :date, author: [:first_name, :last_name]
|
|
160
|
+
|
|
161
|
+
nested :author do
|
|
162
|
+
nested :location do
|
|
163
|
+
filters :city
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Expect a #filters method that returns a hash
|
|
169
|
+
attr_reader :filters
|
|
170
|
+
def initialize(filters: {})
|
|
171
|
+
@filters = filters
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
f = ArticleFilters.new(filters: {
|
|
176
|
+
date: '2025-01-01',
|
|
177
|
+
author: { first_name: 'Ada', last_name: 'Lovelace', location: { city: 'London' } }
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
f.__date # => '2025-01-01'
|
|
181
|
+
f.__author__first_name # => 'Ada'
|
|
182
|
+
f.__author__last_name # => 'Lovelace'
|
|
183
|
+
f.__author__location__city # => 'London'
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Tips:
|
|
187
|
+
- `filter_key_prefix` and `filter_key_infix` control the generated accessor names.
|
|
188
|
+
- Inside the block, `nested :association` affects all `filters` declared within it.
|
data/lib/rokaki/filter_model.rb
CHANGED
|
@@ -120,7 +120,72 @@ module Rokaki
|
|
|
120
120
|
end
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
# Merge two nested like/ilike mappings
|
|
124
|
+
def deep_merge_like(a, b)
|
|
125
|
+
return b if a.nil? || a == {}
|
|
126
|
+
return a if b.nil? || b == {}
|
|
127
|
+
a.merge(b) do |_, v1, v2|
|
|
128
|
+
if v1.is_a?(Hash) && v2.is_a?(Hash)
|
|
129
|
+
deep_merge_like(v1, v2)
|
|
130
|
+
else
|
|
131
|
+
# Prefer later definitions
|
|
132
|
+
v2
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Wrap a normalized mapping with current nested context stack
|
|
138
|
+
def wrap_in_context(mapping)
|
|
139
|
+
return mapping if !@__ctx_stack || @__ctx_stack.empty?
|
|
140
|
+
@__ctx_stack.reverse.inject(mapping) { |acc, key| { key => acc } }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Block DSL: nested context for like/ilike within filter_map block
|
|
144
|
+
def nested(name, &blk)
|
|
145
|
+
if instance_variable_defined?(:@__in_filter_map_block) && @__in_filter_map_block
|
|
146
|
+
raise ArgumentError, 'nested requires a symbol name' unless name.is_a?(Symbol)
|
|
147
|
+
@__ctx_stack << name
|
|
148
|
+
instance_eval(&blk) if blk
|
|
149
|
+
@__ctx_stack.pop
|
|
150
|
+
else
|
|
151
|
+
raise NoMethodError, 'nested can only be used inside filter_map block'
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def filter_map(*args, &block)
|
|
156
|
+
# Block form: requires prior calls to filter_model and define_query_key
|
|
157
|
+
if block_given? && args.empty?
|
|
158
|
+
raise ArgumentError, 'define_query_key must be called before block filter_map' unless @filter_map_query_key
|
|
159
|
+
raise ArgumentError, 'filter_model must be called before block filter_map' unless @model
|
|
160
|
+
@_filter_db ||= :postgres
|
|
161
|
+
|
|
162
|
+
# Enter block-collection mode
|
|
163
|
+
@__in_filter_map_block = true
|
|
164
|
+
@__block_like_accumulator = {}
|
|
165
|
+
@__block_ilike_accumulator = {}
|
|
166
|
+
@__ctx_stack = []
|
|
167
|
+
|
|
168
|
+
instance_eval(&block)
|
|
169
|
+
|
|
170
|
+
# Exit and materialize definitions
|
|
171
|
+
@__in_filter_map_block = false
|
|
172
|
+
unless @__block_like_accumulator.empty?
|
|
173
|
+
like(@__block_like_accumulator)
|
|
174
|
+
end
|
|
175
|
+
unless @__block_ilike_accumulator.empty?
|
|
176
|
+
ilike(@__block_ilike_accumulator)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# cleanup
|
|
180
|
+
@__block_like_accumulator = nil
|
|
181
|
+
@__block_ilike_accumulator = nil
|
|
182
|
+
@__ctx_stack = nil
|
|
183
|
+
|
|
184
|
+
return
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Positional/legacy form
|
|
188
|
+
model, query_key, options = args
|
|
124
189
|
filter_model(model)
|
|
125
190
|
@filter_map_query_key = query_key
|
|
126
191
|
|
|
@@ -143,6 +208,12 @@ module Rokaki
|
|
|
143
208
|
end
|
|
144
209
|
|
|
145
210
|
def filters(*filter_keys)
|
|
211
|
+
# In block form for FilterModel, allow equality filters inside nested contexts
|
|
212
|
+
if instance_variable_defined?(:@__in_filter_map_block) && @__in_filter_map_block
|
|
213
|
+
wrapped_keys = filter_keys.map { |fk| wrap_in_context(fk) }
|
|
214
|
+
filter_keys = wrapped_keys
|
|
215
|
+
end
|
|
216
|
+
|
|
146
217
|
if @filter_map_query_key
|
|
147
218
|
define_filter_map(@filter_map_query_key, *filter_keys)
|
|
148
219
|
else
|
|
@@ -276,6 +347,12 @@ module Rokaki
|
|
|
276
347
|
|
|
277
348
|
def like(args)
|
|
278
349
|
raise ArgumentError, 'argument mush be a hash' unless args.is_a? Hash
|
|
350
|
+
if instance_variable_defined?(:@__in_filter_map_block) && @__in_filter_map_block
|
|
351
|
+
normalized = normalize_like_modes(args)
|
|
352
|
+
@__block_like_accumulator = deep_merge_like(@__block_like_accumulator, wrap_in_context(normalized))
|
|
353
|
+
return
|
|
354
|
+
end
|
|
355
|
+
|
|
279
356
|
normalized = normalize_like_modes(args)
|
|
280
357
|
@_like_semantics = (@_like_semantics || {}).merge(normalized)
|
|
281
358
|
|
|
@@ -285,6 +362,12 @@ module Rokaki
|
|
|
285
362
|
|
|
286
363
|
def ilike(args)
|
|
287
364
|
raise ArgumentError, 'argument mush be a hash' unless args.is_a? Hash
|
|
365
|
+
if instance_variable_defined?(:@__in_filter_map_block) && @__in_filter_map_block
|
|
366
|
+
normalized = normalize_like_modes(args)
|
|
367
|
+
@__block_ilike_accumulator = deep_merge_like(@__block_ilike_accumulator, wrap_in_context(normalized))
|
|
368
|
+
return
|
|
369
|
+
end
|
|
370
|
+
|
|
288
371
|
normalized = normalize_like_modes(args)
|
|
289
372
|
@i_like_semantics = (@i_like_semantics || {}).merge(normalized)
|
|
290
373
|
|
data/lib/rokaki/filterable.rb
CHANGED
|
@@ -13,6 +13,48 @@ module Rokaki
|
|
|
13
13
|
module ClassMethods
|
|
14
14
|
private
|
|
15
15
|
|
|
16
|
+
# --- Block DSL support (Filterable mode) ---
|
|
17
|
+
def nested(name, &blk)
|
|
18
|
+
if instance_variable_defined?(:@__in_filterable_block) && @__in_filterable_block
|
|
19
|
+
raise ArgumentError, 'nested requires a symbol name' unless name.is_a?(Symbol)
|
|
20
|
+
@__ctx_stack << name
|
|
21
|
+
instance_eval(&blk) if blk
|
|
22
|
+
@__ctx_stack.pop
|
|
23
|
+
else
|
|
24
|
+
raise NoMethodError, 'nested can only be used inside filter_map block'
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# In Filterable, `filter_map` without args opens a block to declare `filters` with optional nesting
|
|
29
|
+
# For backward compatibility, if args are provided, delegate to define_filter_map
|
|
30
|
+
def filter_map(*args, &block)
|
|
31
|
+
if block_given? && args.empty?
|
|
32
|
+
# Enter block-collection mode
|
|
33
|
+
@__in_filterable_block = true
|
|
34
|
+
@__ctx_stack = []
|
|
35
|
+
@__block_filters = []
|
|
36
|
+
|
|
37
|
+
instance_eval(&block)
|
|
38
|
+
|
|
39
|
+
# Materialize collected filters
|
|
40
|
+
unless @__block_filters.empty?
|
|
41
|
+
define_filter_keys(*@__block_filters)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# cleanup
|
|
45
|
+
@__in_filterable_block = false
|
|
46
|
+
@__ctx_stack = nil
|
|
47
|
+
@__block_filters = nil
|
|
48
|
+
return
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Positional/legacy map form (delegates to define_filter_map)
|
|
52
|
+
if args.any?
|
|
53
|
+
query_field, *filter_keys = args
|
|
54
|
+
define_filter_map(query_field, *filter_keys)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
16
58
|
def define_filter_keys(*filter_keys)
|
|
17
59
|
filter_keys.each do |filter_key|
|
|
18
60
|
_build_filter([filter_key]) unless Hash === filter_key
|
|
@@ -96,8 +138,8 @@ module Rokaki
|
|
|
96
138
|
if value.is_a? Array
|
|
97
139
|
value.each do |av|
|
|
98
140
|
if av.is_a? Symbol
|
|
99
|
-
|
|
100
|
-
|
|
141
|
+
_keys = keys.dup << av
|
|
142
|
+
yield _keys
|
|
101
143
|
else
|
|
102
144
|
deep_map(keys, av, &block)
|
|
103
145
|
end
|
|
@@ -110,6 +152,33 @@ module Rokaki
|
|
|
110
152
|
end
|
|
111
153
|
end
|
|
112
154
|
|
|
155
|
+
# Helper: wrap a Symbol/Hash filter key in current nested context
|
|
156
|
+
def wrap_in_context(filter_key)
|
|
157
|
+
return filter_key unless instance_variable_defined?(:@__ctx_stack) && @__ctx_stack && !@__ctx_stack.empty?
|
|
158
|
+
ctx = @__ctx_stack.dup
|
|
159
|
+
if filter_key.is_a?(Hash)
|
|
160
|
+
# Nest the entire hash under the context chain
|
|
161
|
+
ctx.reverse.inject(filter_key) { |acc, k| { k => acc } }
|
|
162
|
+
else
|
|
163
|
+
# Symbol → build a hash with leaf
|
|
164
|
+
ctx.reverse.inject(filter_key) { |acc, k| { k => acc } }
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
public
|
|
169
|
+
|
|
170
|
+
# Enhance `filters` to support block mode accumulation
|
|
171
|
+
def filters(*filter_keys)
|
|
172
|
+
if instance_variable_defined?(:@__in_filterable_block) && @__in_filterable_block
|
|
173
|
+
filter_keys.each do |fk|
|
|
174
|
+
@__block_filters << wrap_in_context(fk)
|
|
175
|
+
end
|
|
176
|
+
return
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
define_filter_keys(*filter_keys)
|
|
180
|
+
end
|
|
181
|
+
|
|
113
182
|
end
|
|
114
183
|
|
|
115
184
|
def filters
|
data/lib/rokaki/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rokaki
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Steve Martin
|
|
@@ -248,6 +248,7 @@ files:
|
|
|
248
248
|
- ".rspec"
|
|
249
249
|
- ".ruby-version"
|
|
250
250
|
- ".travis.yml"
|
|
251
|
+
- CHANGELOG.md
|
|
251
252
|
- Gemfile
|
|
252
253
|
- Gemfile.lock
|
|
253
254
|
- Guardfile
|