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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dcbf82920cbfeff8466f6ccd583f1a66652082e087c2d6641806be8720dd8d6c
4
- data.tar.gz: 4165d387e1fba3820fc302ef414f0019077067a1dca7d307943030f0aec99207
3
+ metadata.gz: 2659a359499939511a45107cc281e2aecce654ecf587f668e9de52d6fd8e3424
4
+ data.tar.gz: 4a5dfa2b6eef83fb731bb558575bdd5072b2c5ff2f4dc3670b11cae9487447ff
5
5
  SHA512:
6
- metadata.gz: 8e5d866b175e0799ad9bff2cbb8660e8ebca48785e508223e7583452eb7e95e56c1c35f15ff8625fc456365651b82efff40636579f90469d9909f650f6ee2b1d
7
- data.tar.gz: e974f32a7626a94fb618295ea9600e9747af73bd35986f9e843a81b4bc6168c98180ef85d150fc1ad96ff4fa4700daae5d16d75596bc2d3dd9279ba8d6a98c64
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rokaki (0.12.0)
4
+ rokaki (0.13.0)
5
5
  activesupport
6
6
 
7
7
  GEM
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.11"
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
- Use `Rokaki::FilterModel` and declare mappings with method arguments (no block DSL).
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.11"
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 (no block DSL)
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.
@@ -120,7 +120,72 @@ module Rokaki
120
120
  end
121
121
  end
122
122
 
123
- def filter_map(model, query_key, options)
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
 
@@ -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
- _keys = keys.dup << av
100
- yield _keys
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
@@ -1,3 +1,3 @@
1
1
  module Rokaki
2
- VERSION = "0.12.0"
2
+ VERSION = "0.13.0"
3
3
  end
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.12.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