rokaki 0.11.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/.github/workflows/pages.yml +31 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile.lock +1 -1
- data/docs/Gemfile +17 -0
- data/docs/_config.yml +26 -0
- data/docs/adapters.md +46 -0
- data/docs/configuration.md +63 -0
- data/docs/index.md +113 -0
- data/docs/usage.md +188 -0
- data/lib/rokaki/filter_model.rb +107 -5
- data/lib/rokaki/filterable.rb +71 -2
- data/lib/rokaki/version.rb +1 -1
- metadata +9 -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
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Build documentation (no deploy)
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main, master ]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build-docs:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
env:
|
|
12
|
+
BUNDLE_GEMFILE: docs/Gemfile
|
|
13
|
+
steps:
|
|
14
|
+
- name: Checkout
|
|
15
|
+
uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Setup Ruby
|
|
18
|
+
uses: ruby/setup-ruby@v1
|
|
19
|
+
with:
|
|
20
|
+
ruby-version: '3.3.0'
|
|
21
|
+
bundler-cache: true
|
|
22
|
+
|
|
23
|
+
- name: Build site
|
|
24
|
+
run: |
|
|
25
|
+
bundle exec jekyll build -s docs -d _site
|
|
26
|
+
|
|
27
|
+
- name: Upload built site artifact
|
|
28
|
+
uses: actions/upload-artifact@v4
|
|
29
|
+
with:
|
|
30
|
+
name: site
|
|
31
|
+
path: _site
|
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/Gemfile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# docs/Gemfile
|
|
2
|
+
source "https://rubygems.org"
|
|
3
|
+
|
|
4
|
+
# Static site generator
|
|
5
|
+
gem "jekyll", "~> 4.4"
|
|
6
|
+
|
|
7
|
+
# Theme used by docs/_config.yml
|
|
8
|
+
gem "minima", "~> 2.5"
|
|
9
|
+
|
|
10
|
+
# GitHub-flavored Markdown support
|
|
11
|
+
gem "kramdown-parser-gfm", "~> 1.1"
|
|
12
|
+
|
|
13
|
+
# Ruby 3+ local serving requires webrick
|
|
14
|
+
gem "webrick", "~> 1.8"
|
|
15
|
+
|
|
16
|
+
# Ensure compatibility with workflow expectation
|
|
17
|
+
gem "rake", "~> 13.3"
|
data/docs/_config.yml
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
title: Rokaki
|
|
2
|
+
description: A DSL for filtering data in web requests (ActiveRecord)
|
|
3
|
+
# For project pages (https://<user>.github.io/<repo>), set:
|
|
4
|
+
# url: "https://tevio.github.io"
|
|
5
|
+
# baseurl: "/rokaki"
|
|
6
|
+
# This ensures theme assets like /assets/main.css resolve under /rokaki/ on GitHub Pages.
|
|
7
|
+
baseurl: "/rokaki"
|
|
8
|
+
url: "https://tevio.github.io"
|
|
9
|
+
theme: minima
|
|
10
|
+
|
|
11
|
+
# Build settings
|
|
12
|
+
markdown: kramdown
|
|
13
|
+
kramdown:
|
|
14
|
+
input: GFM
|
|
15
|
+
|
|
16
|
+
# Exclude existing generated RDoc folder from site
|
|
17
|
+
exclude:
|
|
18
|
+
- doc/
|
|
19
|
+
- pkg/
|
|
20
|
+
- spec/
|
|
21
|
+
- Gemfile
|
|
22
|
+
- Gemfile.lock
|
|
23
|
+
- Rakefile
|
|
24
|
+
- rokaki.gemspec
|
|
25
|
+
- Guardfile
|
|
26
|
+
- README.md
|
data/docs/adapters.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: page
|
|
3
|
+
title: Database adapters
|
|
4
|
+
permalink: /adapters
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Rokaki generates adapter‑aware SQL for PostgreSQL, MySQL, and SQL Server.
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
- PostgreSQL
|
|
12
|
+
- Case‑insensitive: `ILIKE`
|
|
13
|
+
- Case‑sensitive: `LIKE`
|
|
14
|
+
- Multi‑term: `ANY (ARRAY[...])`
|
|
15
|
+
- MySQL
|
|
16
|
+
- Case‑insensitive: `LIKE`
|
|
17
|
+
- Case‑sensitive: `LIKE BINARY`
|
|
18
|
+
- Nested‑like filters may use `REGEXP` where designed in the library
|
|
19
|
+
- SQL Server
|
|
20
|
+
- Uses `LIKE` with safe escaping
|
|
21
|
+
- Multi‑term input expands to OR‑chained predicates (e.g., `(col LIKE :q0 OR col LIKE :q1 ...)`) with `ESCAPE '\\'`
|
|
22
|
+
- Case sensitivity follows DB collation by default; future versions may add inline `COLLATE` options
|
|
23
|
+
|
|
24
|
+
## LIKE modes
|
|
25
|
+
|
|
26
|
+
All adapters support the same modes, which you declare via the values in your `like` mapping (there is no `modes:` option):
|
|
27
|
+
|
|
28
|
+
- `prefix` → `%term`
|
|
29
|
+
- `suffix` → `term%`
|
|
30
|
+
- `circumfix` → `%term%` (synonyms supported: `:parafix`, `:confix`, `:ambifix`)
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
# Declare modes via like-mapping values (no block DSL)
|
|
36
|
+
like title: :circumfix
|
|
37
|
+
like author: { first_name: :prefix }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
When you pass an array of terms, Rokaki composes adapter‑appropriate SQL that matches any of the terms.
|
|
41
|
+
|
|
42
|
+
## Notes on case sensitivity
|
|
43
|
+
|
|
44
|
+
- PostgreSQL: `ILIKE` is case‑insensitive; `LIKE` is case‑sensitive depending on collation/LC settings but generally treated as case‑sensitive for ASCII.
|
|
45
|
+
- MySQL: `LIKE` case sensitivity depends on column collation; `LIKE BINARY` forces byte comparison (case‑sensitive for ASCII).
|
|
46
|
+
- SQL Server: The server/database/column collation determines sensitivity. Rokaki currently defers to your DB’s default. If you need deterministic behavior regardless of DB defaults, consider using a case‑sensitive collation on the column or open an issue to discuss inline `COLLATE` options.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: page
|
|
3
|
+
title: Configuration
|
|
4
|
+
permalink: /configuration
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
This page covers configuration, environment overrides, and tips for running the test suite across adapters.
|
|
8
|
+
|
|
9
|
+
## Environment variables
|
|
10
|
+
|
|
11
|
+
Rokaki's test helpers (used in the specs) support environment variable overrides for all adapters. These are useful when your local databases run on non‑default ports or hosts.
|
|
12
|
+
|
|
13
|
+
### SQL Server
|
|
14
|
+
- `SQLSERVER_HOST` (default: `localhost`)
|
|
15
|
+
- `SQLSERVER_PORT` (default: `1433`)
|
|
16
|
+
- `SQLSERVER_USERNAME` (default: `sa`)
|
|
17
|
+
- `SQLSERVER_PASSWORD`
|
|
18
|
+
- `SQLSERVER_DATABASE` (default: `rokaki`)
|
|
19
|
+
|
|
20
|
+
### MySQL
|
|
21
|
+
- `MYSQL_HOST` (default: `127.0.0.1`)
|
|
22
|
+
- `MYSQL_PORT` (default: `3306`)
|
|
23
|
+
- `MYSQL_USERNAME` (default: `rokaki`)
|
|
24
|
+
- `MYSQL_PASSWORD` (default: `rokaki`)
|
|
25
|
+
- `MYSQL_DATABASE` (default: `rokaki`)
|
|
26
|
+
|
|
27
|
+
### PostgreSQL
|
|
28
|
+
- `POSTGRES_HOST` (default: `127.0.0.1`)
|
|
29
|
+
- `POSTGRES_PORT` (default: `5432`)
|
|
30
|
+
- `POSTGRES_USERNAME` (default: `postgres`)
|
|
31
|
+
- `POSTGRES_PASSWORD` (default: `postgres`)
|
|
32
|
+
- `POSTGRES_DATABASE` (default: `rokaki`)
|
|
33
|
+
|
|
34
|
+
## SQL Server notes
|
|
35
|
+
|
|
36
|
+
- Rokaki uses `LIKE` with proper escaping and OR expansion for arrays of terms.
|
|
37
|
+
- Case sensitivity follows your database/column collation. Future versions may allow inline `COLLATE` options.
|
|
38
|
+
|
|
39
|
+
## Running tests locally
|
|
40
|
+
|
|
41
|
+
Ensure you have Ruby (see `.ruby-version`), then install dependencies and run specs.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
bundle install
|
|
45
|
+
./spec/ordered_run.sh
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or run a single adapter suite, for example SQL Server:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bundle exec rspec spec/lib/03_sqlserver_aware_spec.rb
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
If your SQL Server listens on a different port (e.g., 1434), set an override:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
export SQLSERVER_PORT=1434
|
|
58
|
+
bundle exec rspec spec/lib/03_sqlserver_aware_spec.rb
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## GitHub Actions
|
|
62
|
+
|
|
63
|
+
The repository includes CI that starts MySQL (9.4), PostgreSQL (13), and SQL Server (2022) services and runs the ordered spec suite. See `.github/workflows/spec.yml`.
|
data/docs/index.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: home
|
|
3
|
+
title: Rokaki
|
|
4
|
+
permalink: /
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Rokaki is a small Ruby library that helps you build safe, composable filters for ActiveRecord queries in web requests.
|
|
8
|
+
|
|
9
|
+
- Works with PostgreSQL, MySQL, and SQL Server
|
|
10
|
+
- Supports simple and nested filters
|
|
11
|
+
- LIKE-based matching with prefix/suffix/circumfix modes (circumfix also accepts synonyms: parafix, confix, ambifix)
|
|
12
|
+
- Array-of-terms matching (adapter-aware)
|
|
13
|
+
|
|
14
|
+
Get started below or jump to:
|
|
15
|
+
- [Usage](./usage)
|
|
16
|
+
- [Database adapters](./adapters)
|
|
17
|
+
- [Configuration](./configuration)
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
Add to your application's Gemfile:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
gem "rokaki", "~> 0.13"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bundle install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
You can declare mappings in two ways: argument-based (original) or block-form DSL. Both are equivalent.
|
|
36
|
+
|
|
37
|
+
Argument-based form:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
class ArticleQuery
|
|
41
|
+
include Rokaki::FilterModel
|
|
42
|
+
|
|
43
|
+
# Tell Rokaki which model to query and which DB adapter semantics to use
|
|
44
|
+
filter_model :article, db: :postgres # or :mysql, :sqlserver
|
|
45
|
+
|
|
46
|
+
# Map a single query key (:q) to multiple LIKE targets on Article
|
|
47
|
+
define_query_key :q
|
|
48
|
+
like title: :circumfix, content: :circumfix
|
|
49
|
+
|
|
50
|
+
# Nested LIKEs on associated models are expressed with hashes
|
|
51
|
+
like author: { first_name: :prefix, last_name: :suffix }
|
|
52
|
+
|
|
53
|
+
attr_accessor :filters
|
|
54
|
+
def initialize(filters: {})
|
|
55
|
+
@filters = filters
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# In a controller/service:
|
|
60
|
+
filtered = ArticleQuery.new(filters: params).results
|
|
61
|
+
```
|
|
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
|
+
|
|
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.
|
|
92
|
+
|
|
93
|
+
## Matching modes
|
|
94
|
+
|
|
95
|
+
- prefix: matches values that start with given term(s)
|
|
96
|
+
- suffix: matches values that end with given term(s)
|
|
97
|
+
- circumfix: matches values that contain given term(s)
|
|
98
|
+
|
|
99
|
+
All modes accept either a single string or an array of terms.
|
|
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
|
+
|
|
109
|
+
## Next steps
|
|
110
|
+
|
|
111
|
+
- Learn the full DSL and examples in [Usage](./usage)
|
|
112
|
+
- See adapter specifics (PostgreSQL/MySQL/SQL Server) in [Database adapters](./adapters)
|
|
113
|
+
- Configure connections and environment variables in [Configuration](./configuration)
|
data/docs/usage.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: page
|
|
3
|
+
title: Usage
|
|
4
|
+
permalink: /usage
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
This page shows how to use Rokaki to define filters and apply them to ActiveRecord relations.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Add the gem to your Gemfile and bundle:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem "rokaki", "~> 0.13"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Basic setup
|
|
22
|
+
|
|
23
|
+
Include `Rokaki::Filterable` in models you want to filter, and define a `filter_map` with fields and nested associations.
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
class Author < ActiveRecord::Base
|
|
27
|
+
has_many :articles
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class ArticleQuery
|
|
31
|
+
include Rokaki::FilterModel
|
|
32
|
+
belongs_to :author
|
|
33
|
+
|
|
34
|
+
# Choose model and adapter
|
|
35
|
+
filter_model :article, db: :postgres # or :mysql, :sqlserver
|
|
36
|
+
|
|
37
|
+
# Map a single query key (:q) to multiple LIKE targets
|
|
38
|
+
define_query_key :q
|
|
39
|
+
like title: :circumfix, content: :circumfix
|
|
40
|
+
|
|
41
|
+
# Nested LIKEs via hash mapping
|
|
42
|
+
like author: { first_name: :prefix, last_name: :suffix }
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Applying filters
|
|
47
|
+
|
|
48
|
+
Call `Model.filter(params)` to build a relation based on supported keys.
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
params = {
|
|
52
|
+
title_prefix: "Intro",
|
|
53
|
+
q: ["ruby", "rails"],
|
|
54
|
+
author_last_name: "martin"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
filtered = Article.filter(params)
|
|
58
|
+
# => ActiveRecord::Relation (chainable)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
You can keep chaining other scopes/clauses:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
Article.filter(params).order(published: :desc).limit(20)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## LIKE modes and affix options
|
|
68
|
+
|
|
69
|
+
Declare the LIKE mode via the value in your `like` mapping (there is no `modes:` option). For example: `like title: :prefix`.
|
|
70
|
+
|
|
71
|
+
- `prefix` → matches strings that start with a term (pattern: `%term`)
|
|
72
|
+
- `suffix` → matches strings that end with a term (pattern: `term%`)
|
|
73
|
+
- `circumfix` → matches strings that contain a term (pattern: `%term%`)
|
|
74
|
+
- Synonyms supported: `:parafix`, `:confix`, `:ambifix` (all behave the same as `:circumfix`)
|
|
75
|
+
|
|
76
|
+
Each accepts a single string or an array of strings. Rokaki generates adapter‑aware SQL:
|
|
77
|
+
|
|
78
|
+
- PostgreSQL: `LIKE`/`ILIKE` with `ANY (ARRAY[...])`
|
|
79
|
+
- MySQL: `LIKE`/`LIKE BINARY` and, in nested-like contexts, `REGEXP` where designed
|
|
80
|
+
- SQL Server: `LIKE` with safe escaping; arrays expand into OR chains of parameterized `LIKE` predicates
|
|
81
|
+
|
|
82
|
+
## Nested filters
|
|
83
|
+
|
|
84
|
+
Use `nested :association` to scope filters to joined tables. Rokaki handles the necessary joins and qualified columns.
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
filter_map do
|
|
88
|
+
nested :author do
|
|
89
|
+
like :first_name, key: :author_first
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Params would include `author_first`, `author_first_prefix`, etc.
|
|
95
|
+
|
|
96
|
+
## Customization tips
|
|
97
|
+
|
|
98
|
+
- Use `key:` to map a filter to a different params key.
|
|
99
|
+
- Combine multiple filters; Rokaki composes them with `AND` by default.
|
|
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
|
@@ -103,7 +103,89 @@ module Rokaki
|
|
|
103
103
|
|
|
104
104
|
private
|
|
105
105
|
|
|
106
|
-
def
|
|
106
|
+
def normalize_like_modes(obj)
|
|
107
|
+
case obj
|
|
108
|
+
when Hash
|
|
109
|
+
obj.each_with_object({}) do |(k, v), h|
|
|
110
|
+
h[k] = normalize_like_modes(v)
|
|
111
|
+
end
|
|
112
|
+
when Array
|
|
113
|
+
obj.map { |e| normalize_like_modes(e) }
|
|
114
|
+
when Symbol
|
|
115
|
+
# Treat alternative affixes as circumfix
|
|
116
|
+
return :circumfix if [:parafix, :confix, :ambifix].include?(obj)
|
|
117
|
+
obj
|
|
118
|
+
else
|
|
119
|
+
obj
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
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
|
|
107
189
|
filter_model(model)
|
|
108
190
|
@filter_map_query_key = query_key
|
|
109
191
|
|
|
@@ -126,6 +208,12 @@ module Rokaki
|
|
|
126
208
|
end
|
|
127
209
|
|
|
128
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
|
+
|
|
129
217
|
if @filter_map_query_key
|
|
130
218
|
define_filter_map(@filter_map_query_key, *filter_keys)
|
|
131
219
|
else
|
|
@@ -259,17 +347,31 @@ module Rokaki
|
|
|
259
347
|
|
|
260
348
|
def like(args)
|
|
261
349
|
raise ArgumentError, 'argument mush be a hash' unless args.is_a? Hash
|
|
262
|
-
|
|
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
|
+
|
|
356
|
+
normalized = normalize_like_modes(args)
|
|
357
|
+
@_like_semantics = (@_like_semantics || {}).merge(normalized)
|
|
263
358
|
|
|
264
|
-
like_keys = LikeKeys.new(
|
|
359
|
+
like_keys = LikeKeys.new(normalized)
|
|
265
360
|
like_filters(like_keys, term_type: case_sensitive)
|
|
266
361
|
end
|
|
267
362
|
|
|
268
363
|
def ilike(args)
|
|
269
364
|
raise ArgumentError, 'argument mush be a hash' unless args.is_a? Hash
|
|
270
|
-
|
|
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
|
+
|
|
371
|
+
normalized = normalize_like_modes(args)
|
|
372
|
+
@i_like_semantics = (@i_like_semantics || {}).merge(normalized)
|
|
271
373
|
|
|
272
|
-
like_keys = LikeKeys.new(
|
|
374
|
+
like_keys = LikeKeys.new(normalized)
|
|
273
375
|
like_filters(like_keys, term_type: case_insensitive)
|
|
274
376
|
end
|
|
275
377
|
|
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
|
|
@@ -242,11 +242,13 @@ extensions: []
|
|
|
242
242
|
extra_rdoc_files: []
|
|
243
243
|
files:
|
|
244
244
|
- ".github/workflows/codeql-analysis.yml"
|
|
245
|
+
- ".github/workflows/pages.yml"
|
|
245
246
|
- ".github/workflows/spec.yml"
|
|
246
247
|
- ".gitignore"
|
|
247
248
|
- ".rspec"
|
|
248
249
|
- ".ruby-version"
|
|
249
250
|
- ".travis.yml"
|
|
251
|
+
- CHANGELOG.md
|
|
250
252
|
- Gemfile
|
|
251
253
|
- Gemfile.lock
|
|
252
254
|
- Guardfile
|
|
@@ -255,6 +257,12 @@ files:
|
|
|
255
257
|
- Rakefile
|
|
256
258
|
- bin/console
|
|
257
259
|
- bin/setup
|
|
260
|
+
- docs/Gemfile
|
|
261
|
+
- docs/_config.yml
|
|
262
|
+
- docs/adapters.md
|
|
263
|
+
- docs/configuration.md
|
|
264
|
+
- docs/index.md
|
|
265
|
+
- docs/usage.md
|
|
258
266
|
- lib/rokaki.rb
|
|
259
267
|
- lib/rokaki/filter_model.rb
|
|
260
268
|
- lib/rokaki/filter_model/basic_filter.rb
|