rokaki 0.12.0 → 0.14.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/spec.yml +67 -4
- data/CHANGELOG.md +17 -0
- data/Gemfile.lock +18 -10
- data/docs/adapters.md +1 -1
- data/docs/index.md +40 -2
- data/docs/usage.md +90 -2
- data/lib/rokaki/filter_model/basic_filter.rb +8 -0
- data/lib/rokaki/filter_model/nested_filter.rb +3 -2
- data/lib/rokaki/filter_model/nested_like_filters.rb +6 -2
- data/lib/rokaki/filter_model.rb +110 -1
- data/lib/rokaki/filterable.rb +74 -3
- data/lib/rokaki/version.rb +1 -1
- data/rokaki.gemspec +4 -1
- metadata +38 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ed3b16c71be49df063699af31b64190d9aa2db724bd5f6310e458aa33f63025a
|
|
4
|
+
data.tar.gz: c81607decccf33f1e9a71b1e68c3bd478428b22a1f375f9e5554b39ddfc0d8d6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 18d022ef801a2a978f01d1f142c34fb95171131d30f8f8cbe8837a60fc9ea44c162d48e9f672380a965f578de65b568124686399974918497141e0eca549b29a
|
|
7
|
+
data.tar.gz: 77f547a998d588d4128f6df533ae0e555c8561b402e496b53b66c7daee87e42a7ed3b2152ef5101930abd568ee2b0b268aeb71bb75d841feabb102238d776f49
|
data/.github/workflows/spec.yml
CHANGED
|
@@ -35,21 +35,67 @@ jobs:
|
|
|
35
35
|
- 1433:1433
|
|
36
36
|
# No built-in healthcheck; we'll wait in a step using nc
|
|
37
37
|
|
|
38
|
+
oracle:
|
|
39
|
+
image: gvenzl/oracle-free:23-slim
|
|
40
|
+
env:
|
|
41
|
+
ORACLE_PASSWORD: "oracle"
|
|
42
|
+
APP_USER: ROKAKI
|
|
43
|
+
APP_USER_PASSWORD: rokaki
|
|
44
|
+
ports:
|
|
45
|
+
- 1521:1521
|
|
46
|
+
|
|
38
47
|
steps:
|
|
39
48
|
- uses: actions/checkout@v2
|
|
40
49
|
|
|
41
50
|
- name: Install system dependencies
|
|
42
51
|
run: |
|
|
43
52
|
sudo apt-get update
|
|
44
|
-
sudo apt-get install -y build-essential libpq-dev default-libmysqlclient-dev freetds-dev netcat-openbsd
|
|
53
|
+
sudo apt-get install -y build-essential libpq-dev default-libmysqlclient-dev freetds-dev netcat-openbsd unzip curl libaio1t64 libaio-dev
|
|
54
|
+
sudo ln -s /usr/lib/x86_64-linux-gnu/libaio.so.1t64 /usr/lib/x86_64-linux-gnu/libaio.so.1
|
|
45
55
|
|
|
46
56
|
- name: Set up Ruby
|
|
47
57
|
uses: ruby/setup-ruby@v1
|
|
48
58
|
with:
|
|
49
|
-
# Not needed with a .ruby-version file
|
|
50
59
|
ruby-version: 3.3.0
|
|
51
|
-
#
|
|
52
|
-
|
|
60
|
+
bundler-cache: false # we'll run bundle after Instant Client is present
|
|
61
|
+
|
|
62
|
+
- name: Install Oracle Instant Client (ZIP Basic Lite + SDK)
|
|
63
|
+
run: |
|
|
64
|
+
set -e
|
|
65
|
+
sudo mkdir -p /opt/oracle && cd /opt/oracle
|
|
66
|
+
# Download current 23.5 ZIPs directly from Oracle
|
|
67
|
+
curl -fL -o ic-basic.zip https://download.oracle.com/otn_software/linux/instantclient/2326000/instantclient-basic-linux.x64-23.26.0.0.0.zip
|
|
68
|
+
curl -fL -o ic-sdk.zip https://download.oracle.com/otn_software/linux/instantclient/2326000/instantclient-sdk-linux.x64-23.26.0.0.0.zip
|
|
69
|
+
sudo unzip -q -o ic-basic.zip
|
|
70
|
+
sudo unzip -q -o ic-sdk.zip
|
|
71
|
+
ls /opt/oracle
|
|
72
|
+
cd /opt/oracle/instantclient_23_26 && sudo ln -s ../instantclient_23_26 lib
|
|
73
|
+
cd /opt/oracle/instantclient_23_26 && sudo ln -s sdk/include include
|
|
74
|
+
IC_DIR=/opt/oracle/instantclient_23_26
|
|
75
|
+
echo "OCI_DIR=$IC_DIR" | sudo tee -a $GITHUB_ENV
|
|
76
|
+
echo "OCI_LIB_DIR=$IC_DIR" | sudo tee -a $GITHUB_ENV
|
|
77
|
+
echo "OCI_INC_DIR=$IC_DIR/sdk/include" | sudo tee -a $GITHUB_ENV
|
|
78
|
+
echo "LD_LIBRARY_PATH=$IC_DIR:$LD_LIBRARY_PATH" | sudo tee -a $GITHUB_ENV
|
|
79
|
+
|
|
80
|
+
- name: Bundle install (after Instant Client)
|
|
81
|
+
id: bundleInstallAfterInstantClient
|
|
82
|
+
env:
|
|
83
|
+
ORACLE_HOME: /opt/oracle/instantclient_23_26
|
|
84
|
+
OCI_DIR: ${{ env.OCI_DIR }}
|
|
85
|
+
OCI_LIB_DIR: ${{ env.OCI_LIB_DIR }}
|
|
86
|
+
OCI_INC_DIR: ${{ env.OCI_INC_DIR }}
|
|
87
|
+
LD_LIBRARY_PATH: ${{ env.LD_LIBRARY_PATH }}
|
|
88
|
+
run: |
|
|
89
|
+
ls /opt/oracle/instantclient_23_26
|
|
90
|
+
bundle config set build.ruby-oci8 "--with-instant-client-dir=/opt/oracle/instantclient_23_26 --with-instant-client-include=/opt/oracle/instantclient_23_26/sdk/include --with-instant-client-lib=/opt/oracle/instantclient_23_26"
|
|
91
|
+
bundle install --jobs 4 --retry 3
|
|
92
|
+
|
|
93
|
+
- name: Archive Build Artifact
|
|
94
|
+
if: failure() && steps.bundleInstallAfterInstantClient.outcome == 'failure'
|
|
95
|
+
uses: actions/upload-artifact@v4
|
|
96
|
+
with:
|
|
97
|
+
name: build-mkmf-log
|
|
98
|
+
path: /opt/hostedtoolcache/Ruby/3.3.0/x64/lib/ruby/gems/3.3.0/extensions/x86_64-linux/3.3.0/ruby-oci8-2.2.14/mkmf.log
|
|
53
99
|
|
|
54
100
|
- name: Wait for databases to be ready
|
|
55
101
|
shell: bash
|
|
@@ -63,7 +109,24 @@ jobs:
|
|
|
63
109
|
for i in {1..120}; do
|
|
64
110
|
nc -z 127.0.0.1 1433 && echo "SQL Server up" && break || sleep 1
|
|
65
111
|
done
|
|
112
|
+
for i in {1..180}; do
|
|
113
|
+
nc -z 127.0.0.1 1521 && echo "Oracle up" && break || sleep 1
|
|
114
|
+
done
|
|
66
115
|
|
|
67
116
|
- name: Run tests
|
|
117
|
+
env:
|
|
118
|
+
POSTGRES_HOST: 127.0.0.1
|
|
119
|
+
POSTGRES_USERNAME: postgres
|
|
120
|
+
POSTGRES_PASSWORD: postgres
|
|
121
|
+
SQLSERVER_HOST: 127.0.0.1
|
|
122
|
+
SQLSERVER_PORT: 1433
|
|
123
|
+
SQLSERVER_USERNAME: sa
|
|
124
|
+
SQLSERVER_PASSWORD: 5QL5£rv£r
|
|
125
|
+
ORACLE_HOST: 127.0.0.1
|
|
126
|
+
ORACLE_PORT: 1521
|
|
127
|
+
ORACLE_DATABASE: /freepdb1
|
|
128
|
+
ORACLE_USERNAME: ROKAKI
|
|
129
|
+
ORACLE_PASSWORD: rokaki
|
|
130
|
+
LD_LIBRARY_PATH: ${{ env.LD_LIBRARY_PATH }}
|
|
68
131
|
run: |
|
|
69
132
|
./spec/ordered_run.sh
|
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.
|
|
4
|
+
rokaki (0.14.0)
|
|
5
5
|
activesupport
|
|
6
6
|
|
|
7
7
|
GEM
|
|
@@ -13,6 +13,10 @@ GEM
|
|
|
13
13
|
activemodel (= 8.0.3)
|
|
14
14
|
activesupport (= 8.0.3)
|
|
15
15
|
timeout (>= 0.4.0)
|
|
16
|
+
activerecord-oracle_enhanced-adapter (8.0.0)
|
|
17
|
+
activerecord (~> 8.0.0)
|
|
18
|
+
ruby-oci8
|
|
19
|
+
ruby-plsql (>= 0.6.0)
|
|
16
20
|
activerecord-sqlserver-adapter (8.0.9)
|
|
17
21
|
activerecord (~> 8.0.0)
|
|
18
22
|
tiny_tds
|
|
@@ -30,8 +34,8 @@ GEM
|
|
|
30
34
|
tzinfo (~> 2.0, >= 2.0.5)
|
|
31
35
|
uri (>= 0.13.1)
|
|
32
36
|
base64 (0.3.0)
|
|
33
|
-
benchmark (0.
|
|
34
|
-
bigdecimal (3.
|
|
37
|
+
benchmark (0.5.0)
|
|
38
|
+
bigdecimal (3.3.1)
|
|
35
39
|
byebug (12.0.0)
|
|
36
40
|
coderay (1.1.3)
|
|
37
41
|
concurrent-ruby (1.3.5)
|
|
@@ -42,7 +46,7 @@ GEM
|
|
|
42
46
|
database_cleaner-core (2.0.1)
|
|
43
47
|
diff-lcs (1.6.2)
|
|
44
48
|
drb (2.2.3)
|
|
45
|
-
factory_bot (6.5.
|
|
49
|
+
factory_bot (6.5.6)
|
|
46
50
|
activesupport (>= 6.1.0)
|
|
47
51
|
ffi (1.17.2-aarch64-linux-gnu)
|
|
48
52
|
ffi (1.17.2-aarch64-linux-musl)
|
|
@@ -81,7 +85,7 @@ GEM
|
|
|
81
85
|
logger (1.7.0)
|
|
82
86
|
lumberjack (1.4.2)
|
|
83
87
|
method_source (1.1.0)
|
|
84
|
-
minitest (5.
|
|
88
|
+
minitest (5.26.0)
|
|
85
89
|
mysql2 (0.5.7)
|
|
86
90
|
bigdecimal
|
|
87
91
|
nenv (0.3.0)
|
|
@@ -108,19 +112,21 @@ GEM
|
|
|
108
112
|
ffi (~> 1.0)
|
|
109
113
|
reline (0.6.2)
|
|
110
114
|
io-console (~> 0.5)
|
|
111
|
-
rspec (3.13.
|
|
115
|
+
rspec (3.13.2)
|
|
112
116
|
rspec-core (~> 3.13.0)
|
|
113
117
|
rspec-expectations (~> 3.13.0)
|
|
114
118
|
rspec-mocks (~> 3.13.0)
|
|
115
|
-
rspec-core (3.13.
|
|
119
|
+
rspec-core (3.13.6)
|
|
116
120
|
rspec-support (~> 3.13.0)
|
|
117
121
|
rspec-expectations (3.13.5)
|
|
118
122
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
119
123
|
rspec-support (~> 3.13.0)
|
|
120
|
-
rspec-mocks (3.13.
|
|
124
|
+
rspec-mocks (3.13.6)
|
|
121
125
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
122
126
|
rspec-support (~> 3.13.0)
|
|
123
127
|
rspec-support (3.13.6)
|
|
128
|
+
ruby-oci8 (2.2.14)
|
|
129
|
+
ruby-plsql (0.8.0)
|
|
124
130
|
securerandom (0.4.1)
|
|
125
131
|
shellany (0.0.1)
|
|
126
132
|
sqlite3 (2.7.4-aarch64-linux-gnu)
|
|
@@ -147,7 +153,7 @@ GEM
|
|
|
147
153
|
bigdecimal (~> 3)
|
|
148
154
|
tzinfo (2.0.6)
|
|
149
155
|
concurrent-ruby (~> 1.0)
|
|
150
|
-
uri (1.0.
|
|
156
|
+
uri (1.0.4)
|
|
151
157
|
|
|
152
158
|
PLATFORMS
|
|
153
159
|
aarch64-linux
|
|
@@ -164,7 +170,8 @@ PLATFORMS
|
|
|
164
170
|
x86_64-linux-musl
|
|
165
171
|
|
|
166
172
|
DEPENDENCIES
|
|
167
|
-
activerecord
|
|
173
|
+
activerecord (>= 7.1, < 9.0)
|
|
174
|
+
activerecord-oracle_enhanced-adapter (~> 8.0.0)
|
|
168
175
|
activerecord-sqlserver-adapter
|
|
169
176
|
bundler (~> 2.0)
|
|
170
177
|
database_cleaner-active_record
|
|
@@ -178,6 +185,7 @@ DEPENDENCIES
|
|
|
178
185
|
rake (~> 13.0)
|
|
179
186
|
rokaki!
|
|
180
187
|
rspec (~> 3.0)
|
|
188
|
+
ruby-oci8
|
|
181
189
|
sqlite3
|
|
182
190
|
tiny_tds
|
|
183
191
|
|
data/docs/adapters.md
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.
|
|
@@ -40,6 +40,8 @@ module Rokaki
|
|
|
40
40
|
'LIKE BINARY'
|
|
41
41
|
elsif db == :sqlserver
|
|
42
42
|
'LIKE'
|
|
43
|
+
elsif db == :oracle
|
|
44
|
+
'LIKE'
|
|
43
45
|
else
|
|
44
46
|
'LIKE'
|
|
45
47
|
end
|
|
@@ -52,6 +54,9 @@ module Rokaki
|
|
|
52
54
|
'LIKE'
|
|
53
55
|
elsif db == :sqlserver
|
|
54
56
|
'LIKE'
|
|
57
|
+
elsif db == :oracle
|
|
58
|
+
# Use 'ILIKE' as a signal; oracle_like will translate to UPPER(column) LIKE UPPER(:q)
|
|
59
|
+
'ILIKE'
|
|
55
60
|
else
|
|
56
61
|
'LIKE'
|
|
57
62
|
end
|
|
@@ -105,6 +110,9 @@ module Rokaki
|
|
|
105
110
|
elsif db == :sqlserver
|
|
106
111
|
# Delegate to helper that supports arrays and escaping with ESCAPE
|
|
107
112
|
query = "sqlserver_like(@model, \"#{key}\", \"#{type}\", #{filter}, :#{mode})"
|
|
113
|
+
elsif db == :oracle
|
|
114
|
+
# Oracle helper handles case-insensitive via UPPER() when type is 'ILIKE'
|
|
115
|
+
query = "oracle_like(@model, \"#{key}\", \"#{type}\", #{filter}, :#{mode})"
|
|
108
116
|
else
|
|
109
117
|
query = "@model.where(\"#{key} #{type} :query\", "
|
|
110
118
|
query += "query: \"%\#{#{filter}}%\")" if mode == :circumfix
|
|
@@ -139,10 +139,11 @@ module Rokaki
|
|
|
139
139
|
where = where.join
|
|
140
140
|
|
|
141
141
|
if search_mode
|
|
142
|
-
if db == :sqlserver
|
|
142
|
+
if db == :sqlserver || db == :oracle
|
|
143
143
|
key_leaf = "#{keys.last.to_s.pluralize}.#{leaf}"
|
|
144
|
+
helper = db == :sqlserver ? 'sqlserver_like' : 'oracle_like'
|
|
144
145
|
@filter_methods << "def #{prefix}filter#{infix}#{name};"\
|
|
145
|
-
"
|
|
146
|
+
"#{helper}(@model.joins(#{joins}), \"#{key_leaf}\", \"#{type}\", #{prefix}#{name}, :#{search_mode}); end;"
|
|
146
147
|
|
|
147
148
|
@filter_templates << "@model = #{prefix}filter#{infix}#{name} if #{prefix}#{name};"
|
|
148
149
|
else
|
|
@@ -197,7 +197,7 @@ module Rokaki
|
|
|
197
197
|
# Compute key_leaf (qualified column) like other branches
|
|
198
198
|
key_leaf = keys.last ? "#{keys.last.to_s.pluralize}.#{leaf}" : leaf
|
|
199
199
|
|
|
200
|
-
if db == :sqlserver
|
|
200
|
+
if db == :sqlserver || db == :oracle
|
|
201
201
|
# Build relation base with joins
|
|
202
202
|
if join_map.empty?
|
|
203
203
|
rel_expr = "@model"
|
|
@@ -207,7 +207,11 @@ module Rokaki
|
|
|
207
207
|
rel_expr = "@model.joins(**#{join_map})"
|
|
208
208
|
end
|
|
209
209
|
|
|
210
|
-
|
|
210
|
+
if db == :sqlserver
|
|
211
|
+
filter_query = "sqlserver_like(#{rel_expr}, \"#{key_leaf}\", \"#{type.to_s.upcase}\", #{filter_name}, :#{search_mode})"
|
|
212
|
+
else
|
|
213
|
+
filter_query = "oracle_like(#{rel_expr}, \"#{key_leaf}\", \"#{type.to_s.upcase}\", #{filter_name}, :#{search_mode})"
|
|
214
|
+
end
|
|
211
215
|
else
|
|
212
216
|
query = build_like_query(
|
|
213
217
|
type: type,
|
data/lib/rokaki/filter_model.rb
CHANGED
|
@@ -69,6 +69,27 @@ module Rokaki
|
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
# Compose an Oracle LIKE relation supporting arrays of terms and case-insensitive path via UPPER()
|
|
73
|
+
# type_signal: 'LIKE' for case-sensitive semantics; 'ILIKE' to indicate case-insensitive (we will translate)
|
|
74
|
+
def oracle_like(model, column, type_signal, value, mode)
|
|
75
|
+
terms = prepare_like_terms(value, mode)
|
|
76
|
+
ci = (type_signal.to_s.upcase == 'ILIKE')
|
|
77
|
+
col_expr = ci ? "UPPER(#{column})" : column
|
|
78
|
+
build_term = proc { |t| ci ? t.to_s.upcase : t }
|
|
79
|
+
|
|
80
|
+
if terms.is_a?(Array)
|
|
81
|
+
return model.none if terms.empty?
|
|
82
|
+
first = build_term.call(terms[0])
|
|
83
|
+
rel = model.where("#{col_expr} LIKE :q0 ESCAPE '\\'", q0: first)
|
|
84
|
+
terms[1..-1]&.each_with_index do |t, i|
|
|
85
|
+
rel = rel.or(model.where("#{col_expr} LIKE :q#{i + 1} ESCAPE '\\'", "q#{i + 1}".to_sym => build_term.call(t)))
|
|
86
|
+
end
|
|
87
|
+
rel
|
|
88
|
+
else
|
|
89
|
+
model.where("#{col_expr} LIKE :q ESCAPE '\\'", q: build_term.call(terms))
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
72
93
|
def prepare_regex_terms(param, mode)
|
|
73
94
|
if Array === param
|
|
74
95
|
param_map = param.map { |term| ".*#{term}.*" } if mode == :circumfix
|
|
@@ -120,7 +141,72 @@ module Rokaki
|
|
|
120
141
|
end
|
|
121
142
|
end
|
|
122
143
|
|
|
123
|
-
|
|
144
|
+
# Merge two nested like/ilike mappings
|
|
145
|
+
def deep_merge_like(a, b)
|
|
146
|
+
return b if a.nil? || a == {}
|
|
147
|
+
return a if b.nil? || b == {}
|
|
148
|
+
a.merge(b) do |_, v1, v2|
|
|
149
|
+
if v1.is_a?(Hash) && v2.is_a?(Hash)
|
|
150
|
+
deep_merge_like(v1, v2)
|
|
151
|
+
else
|
|
152
|
+
# Prefer later definitions
|
|
153
|
+
v2
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Wrap a normalized mapping with current nested context stack
|
|
159
|
+
def wrap_in_context(mapping)
|
|
160
|
+
return mapping if !@__ctx_stack || @__ctx_stack.empty?
|
|
161
|
+
@__ctx_stack.reverse.inject(mapping) { |acc, key| { key => acc } }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Block DSL: nested context for like/ilike within filter_map block
|
|
165
|
+
def nested(name, &blk)
|
|
166
|
+
if instance_variable_defined?(:@__in_filter_map_block) && @__in_filter_map_block
|
|
167
|
+
raise ArgumentError, 'nested requires a symbol name' unless name.is_a?(Symbol)
|
|
168
|
+
@__ctx_stack << name
|
|
169
|
+
instance_eval(&blk) if blk
|
|
170
|
+
@__ctx_stack.pop
|
|
171
|
+
else
|
|
172
|
+
raise NoMethodError, 'nested can only be used inside filter_map block'
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def filter_map(*args, &block)
|
|
177
|
+
# Block form: requires prior calls to filter_model and define_query_key
|
|
178
|
+
if block_given? && args.empty?
|
|
179
|
+
raise ArgumentError, 'define_query_key must be called before block filter_map' unless @filter_map_query_key
|
|
180
|
+
raise ArgumentError, 'filter_model must be called before block filter_map' unless @model
|
|
181
|
+
@_filter_db ||= :postgres
|
|
182
|
+
|
|
183
|
+
# Enter block-collection mode
|
|
184
|
+
@__in_filter_map_block = true
|
|
185
|
+
@__block_like_accumulator = {}
|
|
186
|
+
@__block_ilike_accumulator = {}
|
|
187
|
+
@__ctx_stack = []
|
|
188
|
+
|
|
189
|
+
instance_eval(&block)
|
|
190
|
+
|
|
191
|
+
# Exit and materialize definitions
|
|
192
|
+
@__in_filter_map_block = false
|
|
193
|
+
unless @__block_like_accumulator.empty?
|
|
194
|
+
like(@__block_like_accumulator)
|
|
195
|
+
end
|
|
196
|
+
unless @__block_ilike_accumulator.empty?
|
|
197
|
+
ilike(@__block_ilike_accumulator)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# cleanup
|
|
201
|
+
@__block_like_accumulator = nil
|
|
202
|
+
@__block_ilike_accumulator = nil
|
|
203
|
+
@__ctx_stack = nil
|
|
204
|
+
|
|
205
|
+
return
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Positional/legacy form
|
|
209
|
+
model, query_key, options = args
|
|
124
210
|
filter_model(model)
|
|
125
211
|
@filter_map_query_key = query_key
|
|
126
212
|
|
|
@@ -143,6 +229,12 @@ module Rokaki
|
|
|
143
229
|
end
|
|
144
230
|
|
|
145
231
|
def filters(*filter_keys)
|
|
232
|
+
# In block form for FilterModel, allow equality filters inside nested contexts
|
|
233
|
+
if instance_variable_defined?(:@__in_filter_map_block) && @__in_filter_map_block
|
|
234
|
+
wrapped_keys = filter_keys.map { |fk| wrap_in_context(fk) }
|
|
235
|
+
filter_keys = wrapped_keys
|
|
236
|
+
end
|
|
237
|
+
|
|
146
238
|
if @filter_map_query_key
|
|
147
239
|
define_filter_map(@filter_map_query_key, *filter_keys)
|
|
148
240
|
else
|
|
@@ -256,6 +348,8 @@ module Rokaki
|
|
|
256
348
|
'REGEXP'
|
|
257
349
|
elsif @_filter_db == :sqlserver
|
|
258
350
|
'LIKE'
|
|
351
|
+
elsif @_filter_db == :oracle
|
|
352
|
+
'LIKE'
|
|
259
353
|
else
|
|
260
354
|
'LIKE'
|
|
261
355
|
end
|
|
@@ -269,6 +363,9 @@ module Rokaki
|
|
|
269
363
|
'REGEXP'
|
|
270
364
|
elsif @_filter_db == :sqlserver
|
|
271
365
|
'LIKE'
|
|
366
|
+
elsif @_filter_db == :oracle
|
|
367
|
+
# Use 'ILIKE' as a signal for case-insensitive; oracle_like will translate to UPPER(column) LIKE UPPER(:q)
|
|
368
|
+
'ILIKE'
|
|
272
369
|
else
|
|
273
370
|
'LIKE'
|
|
274
371
|
end
|
|
@@ -276,6 +373,12 @@ module Rokaki
|
|
|
276
373
|
|
|
277
374
|
def like(args)
|
|
278
375
|
raise ArgumentError, 'argument mush be a hash' unless args.is_a? Hash
|
|
376
|
+
if instance_variable_defined?(:@__in_filter_map_block) && @__in_filter_map_block
|
|
377
|
+
normalized = normalize_like_modes(args)
|
|
378
|
+
@__block_like_accumulator = deep_merge_like(@__block_like_accumulator, wrap_in_context(normalized))
|
|
379
|
+
return
|
|
380
|
+
end
|
|
381
|
+
|
|
279
382
|
normalized = normalize_like_modes(args)
|
|
280
383
|
@_like_semantics = (@_like_semantics || {}).merge(normalized)
|
|
281
384
|
|
|
@@ -285,6 +388,12 @@ module Rokaki
|
|
|
285
388
|
|
|
286
389
|
def ilike(args)
|
|
287
390
|
raise ArgumentError, 'argument mush be a hash' unless args.is_a? Hash
|
|
391
|
+
if instance_variable_defined?(:@__in_filter_map_block) && @__in_filter_map_block
|
|
392
|
+
normalized = normalize_like_modes(args)
|
|
393
|
+
@__block_ilike_accumulator = deep_merge_like(@__block_ilike_accumulator, wrap_in_context(normalized))
|
|
394
|
+
return
|
|
395
|
+
end
|
|
396
|
+
|
|
288
397
|
normalized = normalize_like_modes(args)
|
|
289
398
|
@i_like_semantics = (@i_like_semantics || {}).merge(normalized)
|
|
290
399
|
|
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
|
|
@@ -35,7 +77,7 @@ module Rokaki
|
|
|
35
77
|
@filter_key_prefix ||= prefix
|
|
36
78
|
end
|
|
37
79
|
|
|
38
|
-
def filter_key_infix(infix = :
|
|
80
|
+
def filter_key_infix(infix = :__)
|
|
39
81
|
@filter_key_infix ||= infix
|
|
40
82
|
end
|
|
41
83
|
|
|
@@ -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,35 @@ 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
|
+
@__block_filters ||= []
|
|
174
|
+
@__ctx_stack ||= []
|
|
175
|
+
filter_keys.each do |fk|
|
|
176
|
+
@__block_filters << wrap_in_context(fk)
|
|
177
|
+
end
|
|
178
|
+
return
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
define_filter_keys(*filter_keys)
|
|
182
|
+
end
|
|
183
|
+
|
|
113
184
|
end
|
|
114
185
|
|
|
115
186
|
def filters
|
data/lib/rokaki/version.rb
CHANGED
data/rokaki.gemspec
CHANGED
|
@@ -32,7 +32,7 @@ Gem::Specification.new do |spec|
|
|
|
32
32
|
|
|
33
33
|
spec.add_dependency 'activesupport'
|
|
34
34
|
|
|
35
|
-
spec.add_development_dependency 'activerecord'
|
|
35
|
+
spec.add_development_dependency 'activerecord', '>= 7.1', '< 9.0'
|
|
36
36
|
spec.add_development_dependency 'bundler', '~> 2.0'
|
|
37
37
|
spec.add_development_dependency 'pry'
|
|
38
38
|
spec.add_development_dependency 'pry-byebug'
|
|
@@ -50,5 +50,8 @@ Gem::Specification.new do |spec|
|
|
|
50
50
|
# For SQL Server testing
|
|
51
51
|
spec.add_development_dependency 'tiny_tds'
|
|
52
52
|
spec.add_development_dependency 'activerecord-sqlserver-adapter'
|
|
53
|
+
# For Oracle testing (optional). Enable by setting WITH_ORACLE=1 before bundling.
|
|
54
|
+
spec.add_development_dependency 'ruby-oci8'
|
|
55
|
+
spec.add_development_dependency 'activerecord-oracle_enhanced-adapter', '~> 8.0.0'
|
|
53
56
|
|
|
54
57
|
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.
|
|
4
|
+
version: 0.14.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Steve Martin
|
|
@@ -30,14 +30,20 @@ dependencies:
|
|
|
30
30
|
requirements:
|
|
31
31
|
- - ">="
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
33
|
+
version: '7.1'
|
|
34
|
+
- - "<"
|
|
35
|
+
- !ruby/object:Gem::Version
|
|
36
|
+
version: '9.0'
|
|
34
37
|
type: :development
|
|
35
38
|
prerelease: false
|
|
36
39
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
40
|
requirements:
|
|
38
41
|
- - ">="
|
|
39
42
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '
|
|
43
|
+
version: '7.1'
|
|
44
|
+
- - "<"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '9.0'
|
|
41
47
|
- !ruby/object:Gem::Dependency
|
|
42
48
|
name: bundler
|
|
43
49
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -234,6 +240,34 @@ dependencies:
|
|
|
234
240
|
- - ">="
|
|
235
241
|
- !ruby/object:Gem::Version
|
|
236
242
|
version: '0'
|
|
243
|
+
- !ruby/object:Gem::Dependency
|
|
244
|
+
name: ruby-oci8
|
|
245
|
+
requirement: !ruby/object:Gem::Requirement
|
|
246
|
+
requirements:
|
|
247
|
+
- - ">="
|
|
248
|
+
- !ruby/object:Gem::Version
|
|
249
|
+
version: '0'
|
|
250
|
+
type: :development
|
|
251
|
+
prerelease: false
|
|
252
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
253
|
+
requirements:
|
|
254
|
+
- - ">="
|
|
255
|
+
- !ruby/object:Gem::Version
|
|
256
|
+
version: '0'
|
|
257
|
+
- !ruby/object:Gem::Dependency
|
|
258
|
+
name: activerecord-oracle_enhanced-adapter
|
|
259
|
+
requirement: !ruby/object:Gem::Requirement
|
|
260
|
+
requirements:
|
|
261
|
+
- - "~>"
|
|
262
|
+
- !ruby/object:Gem::Version
|
|
263
|
+
version: 8.0.0
|
|
264
|
+
type: :development
|
|
265
|
+
prerelease: false
|
|
266
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
267
|
+
requirements:
|
|
268
|
+
- - "~>"
|
|
269
|
+
- !ruby/object:Gem::Version
|
|
270
|
+
version: 8.0.0
|
|
237
271
|
description: A dsl for filtering data in web requests
|
|
238
272
|
email:
|
|
239
273
|
- steve@martian.media
|
|
@@ -248,6 +282,7 @@ files:
|
|
|
248
282
|
- ".rspec"
|
|
249
283
|
- ".ruby-version"
|
|
250
284
|
- ".travis.yml"
|
|
285
|
+
- CHANGELOG.md
|
|
251
286
|
- Gemfile
|
|
252
287
|
- Gemfile.lock
|
|
253
288
|
- Guardfile
|