query-stream 1.4.0 → 1.5.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 +35 -1
- data/README.md +155 -4
- data/RELEASE_NOTE.md +39 -0
- data/lib/query_stream/command.rb +2 -7
- data/lib/query_stream/configuration.rb +0 -2
- data/lib/query_stream/data_resolver.rb +2 -6
- data/lib/query_stream/errors.rb +0 -4
- data/lib/query_stream/filter_engine.rb +102 -25
- data/lib/query_stream/query_stream_parser.rb +48 -35
- data/lib/query_stream/singularize.rb +0 -2
- data/lib/query_stream/template_compiler.rb +88 -8
- data/lib/query_stream/version.rb +1 -1
- data/lib/query_stream.rb +10 -25
- data/query-stream.gemspec +7 -8
- metadata +21 -8
- data/Gemfile +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0386ceba9bb9c31fb12bc7d02a7ef3ab23e1df189738e496e6bca25dd14b1cc9'
|
|
4
|
+
data.tar.gz: 1bc4884b4426a0bd2bd76ffa5c1f865742857905f7ba2c4dd2a85e57c1b5aaab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9157468004852779de4ceb88f3ee7cf248ab2c88da7a97b6a2126831f9eaf2e24016909cea8283ac11bbdcbd909da15d576fd121cfea08403f832e4d996afb48
|
|
7
|
+
data.tar.gz: 1a92018c0677b067a487d07a51c6f337daa7bdb66ec3282aa0e75fec2e2f402ba090c23d02a0ec1f5746ecd4b5fb17c74137a4114793fea626dc251dd55c6481
|
data/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
10
10
|
- パフォーマンス最適化
|
|
11
11
|
- 追加のテンプレートスタイル
|
|
12
12
|
|
|
13
|
+
## [Unreleased]
|
|
14
|
+
|
|
15
|
+
## [1.5.0] - 2026-11-01
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **値の無い項目の部分だけを消せるようにした** (`template_compiler.rb`): 変数が 2 つ以上ある行は、区切り(`/`・`・`・`、`・`,`・`,` と、前後に空白のある ` / `)と、変数を含む括弧(`(…)`・`(…)`)で部品に分け、値の無い変数を含む部品だけを、区切りを詰めて消す。`= author.name 著/= publisher/= year 年` は、発行年の分からない本でも `高橋征義 著/SBクリエイティブ` と出る(これまでは著者まで行ごと消えていた)。`= author.name 著(= year年)` も、発行年が無ければ `高橋征義 著` になる。部品が 1 つも残らなければ、今までどおり行ごと消す。すべての値がそろった行の出力は変わらない。
|
|
19
|
+
- 変数が 1 つだけの行と、表の行(`|` で始まる行)は今までどおり行ごと消す(セルを消すと列がずれる)。
|
|
20
|
+
- 行頭の Markdown の目印(`- `・`1. `・`> `・`# `)は、先頭の部品が消えても残す。
|
|
21
|
+
- 空白なしの半角の `/` は区切りにせず(URL を割らない)、`]` の直後の括弧(リンク先)も部品にしない。
|
|
22
|
+
- **抽出条件の数を、アンダースコアで桁区切りして書けるようにした** (`query_stream_parser.rb` / `filter_engine.rb`): `population>=9_000_000` や `population=900_0000..` のように書ける。Ruby の数値リテラルと同じく区切りは数字と数字のあいだにだけ置け、区切る位置は問わない(万の位で区切る `900_0000` も書ける)。比較(`>=` など)・範囲(`..`)・主キー検索に加え、等値(`=`)でも区切りを外して比べる。等値ではデータ側の値からも区切りを外すので、データに `"2024_01"` のような文字列があっても同じ綴りの条件で一致する。`snake_case` のような数でない値のアンダースコアはそのまま残す。
|
|
23
|
+
- **日付を和暦(`R06-01-01`)でも書けるようにした** (`filter_engine.rb`): データでも抽出条件でも、西暦の `2024-01-01` と同じ日付として比べ、並べ替える。元号は頭文字(M・T・S・H・R)で書き、年は 1 桁でもよい(`R6-01-01`)。等値(`=`)でも和暦と西暦を同じ日として扱う。変換は `Date.jisx0301` に任せた——`Date.parse` は `"R06-01-01"` を 2006-01-01 と読み違えるため使えない。元号の境目は検査しない(`H31-05-01` も 2019-05-01 として読む)。存在しない日(`R06-02-30`)は、西暦と同じく `InvalidDateError` になる。
|
|
24
|
+
|
|
25
|
+
- **抽出条件に `OR` / `or` を足した** (`query_stream_parser.rb` / `filter_engine.rb`): `tags=ruby OR year>=2020` のように、別の項目の条件もつなげる。AND は OR より強く結び付き、`a=1 AND b=2 OR c=3` は「a かつ b」または「c」になる。2 件目以降の項目名は省ける(`tags=ruby OR go`)。`||` は記法の区切りの `|` と衝突するので扱わない。前後に空白のある `OR` / `or` は区切りになるので、抽出条件の値に書けなくなった(`title=Rock or Pop`)。値だけの主キー検索(`= book | Rock or Pop`)は今までどおり 1 つの検索語として扱う。
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- **項目名を省いた AND が OR として動いていた** (`query_stream_parser.rb`): `tags=ruby AND beginner` は、`ruby` と `beginner` の両方を含むレコードを選ぶ仕様だが、値を前の条件の値リストへ足していたため、どちらかを含むレコードを選んでいた。別の条件として組み立てる。誤った動きを期待していたテストも直した。
|
|
29
|
+
- **比較とソートの対象の項目に値が無いレコードや比較できない値があると、`ArgumentError` が `render` の外へ出ていた** (`filter_engine.rb`): `on_error` にも渡らず、展開全体が止まった。比較では該当のレコードを除き、ソートでは最も多い種類の値を比べ、それ以外の値と値が無いレコードを昇順でも降順でも末尾に置く。
|
|
30
|
+
- **両端を比較できない範囲で `ArgumentError` が出ていた** (`query_stream_parser.rb` / `filter_engine.rb`): `year=2020..abc` は `Range` を作れない。文字列の値として扱い、どのレコードにも一致しない。
|
|
31
|
+
- **省ける項目が最初の 1 件に無いと、打ち間違いとして止まっていた** (`template_compiler.rb`): テンプレートのキーを確かめるとき、最初の 1 件のキーしか見ていなかった。1 冊目に `year` が無いだけで `UnknownKeyError` になり、値の無い項目を消す仕組みが働く前に止まった。どれか 1 件にあるキーは正しいキーとみなす。どの件にも無いキーは、今までどおり打ち間違いとして知らせる。
|
|
32
|
+
- **日付の範囲指定が 1 件も返さなかった** (`filter_engine.rb`): `date=2024-06-01..2024-06-30` のような範囲は、両端が文字列のまま比べられ、日付に直したデータの値を含まなかった。両端も比較用にそろえてから判定する。数の範囲は両端が最初から数なので影響を受けていなかった。
|
|
33
|
+
- **昇順ソートの書き方が README に無かった** (`README.md`): 例に並ぶのは降順(`-title`)だけで、昇順の `+title` はどこにも出てこない。実装は昇順を持っている——出口が塞がっていただけである。
|
|
34
|
+
|
|
35
|
+
自然に思いつくのは符号を付けない `= books | title` のほうだが、これは**主キー検索**として解釈され、該当なしの警告とともに何も展開されない(`= book | 楽しいRuby` と綴りが同じで、符号の有無だけが見分ける手掛かりになっている)。警告は出るので黙って壊れはしないが、書いた本人は「ソートしたつもり」なので、読んで気づける場所に書いておく必要があった。例へ `+title` / `-year` を足し、符号が必須である理由も添えた。
|
|
36
|
+
|
|
37
|
+
- **一度も実行されない分岐が残っていた** (`query_stream_parser.rb`): `parse_sort` は符号を省いた `title` を昇順として扱う分岐(`/\A?(.+)\z/`)を持ち、コメントも受け付ける綴りとして `"title"` を挙げていた。しかし振り分け側(`parse_segment`)は `/\A[+-][a-zA-Z_]/` に一致した綴りしか渡さないため、**符号なしはここへ届かない**。
|
|
38
|
+
|
|
39
|
+
素の名前を昇順にすると主キー検索と区別が付かなくなるので、**いまの仕様のほうが正しい**。分岐とコメントが古かったので、符号が必須であることを前提に書き直した。挙動は変えていない(`+` / `-` の両方を実測で確認)。
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- **対応 Ruby を 3.4.10 / 4.0.9 にした**。4.0.9 は 2026-10-23 公開。
|
|
43
|
+
- **依存を最新へ上げた**: Minitest → 6.0.6、samovar 2.4.1 → 2.5.1、rake 13.3.1 → 13.4.2。gemspec の minitest は `~> 5.22` から `>= 5.22` へ緩めた——5 系に固定する理由が無く、6 でも 158 件すべて通る(`stub` / `mock` を使っていないため、6 での `minitest/mock` 分離の影響も受けない)。
|
|
44
|
+
- **rubocop を入れた**(`.rubocop.yml`・`rake rubocop`・CI)。見るのは `Lint` と `Security` だけで、引用符や行数は見ない。他の 2 つの gem と同じ構えである。現状 0 件。
|
|
45
|
+
- **README と CHANGELOG の「vivlio-style」を「Vivlio Starter」にした**。製品名を崩した綴りだった。
|
|
46
|
+
|
|
13
47
|
## [1.4.0] - 2026-08-07
|
|
14
48
|
|
|
15
49
|
### Removed
|
|
@@ -115,7 +149,7 @@ QueryStream 1.0.0 としてリリース!YAML/JSON データファイルとテ
|
|
|
115
149
|
### 📚 ドキュメント整備
|
|
116
150
|
- 詳細な README と具体例
|
|
117
151
|
- 完全な CHANGELOG
|
|
118
|
-
-
|
|
152
|
+
- Vivlio Starter での実装例
|
|
119
153
|
|
|
120
154
|
### 🔧 技術的特徴
|
|
121
155
|
- **Ruby 4.0+** モダン開発標準準拠
|
data/README.md
CHANGED
|
@@ -38,12 +38,118 @@ result = QueryStream.render(source, data_dir: 'data', templates_dir: 'templates'
|
|
|
38
38
|
```
|
|
39
39
|
= books # 全件展開
|
|
40
40
|
= books | tags=ruby # タグで絞り込み
|
|
41
|
+
= books | +title # タイトルの昇順
|
|
42
|
+
= books | -year # 発行年の降順
|
|
41
43
|
= books | tags=ruby | -title | 5 # 絞り込み+降順ソート+5件
|
|
44
|
+
= prefectures | population>=9_000_000 # 数は _ で桁を区切れる(900_0000 も可)
|
|
45
|
+
= weather_reports | date>=R06-01-01 # 日付は西暦(2024-01-01)でも和暦でも書ける
|
|
42
46
|
= book | 楽しいRuby # 主キーで一件検索
|
|
43
47
|
= books | :full # fullスタイルで展開
|
|
44
48
|
```
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
**ソートには符号を必ず付けてください**(`+` が昇順、`-` が降順)。符号を省いた
|
|
51
|
+
`= books | title` はソートではなく**主キー検索**として扱われ、該当なしの警告とともに
|
|
52
|
+
何も展開されません。`= book | 楽しいRuby` のような一件検索と綴りが同じになるため、
|
|
53
|
+
符号の有無で見分けています。
|
|
54
|
+
|
|
55
|
+
ソートでは、最も多い種類の値(数・日付・文字列)を比べます。値が無いレコードと、それ以外の
|
|
56
|
+
種類の値のレコードは、昇順でも降順でも末尾に置きます。
|
|
57
|
+
|
|
58
|
+
日付は、データでも抽出条件でも `2024-01-01`(西暦)か `R06-01-01`(和暦)で書きます。
|
|
59
|
+
和暦の元号は頭文字で書き(`M` 明治・`T` 大正・`S` 昭和・`H` 平成・`R` 令和)、年は 1 桁でも
|
|
60
|
+
構いません(`R6-01-01`)。どちらで書いても同じ日付として比べ、並べ替えます。
|
|
61
|
+
|
|
62
|
+
### 指定の順序
|
|
63
|
+
|
|
64
|
+
`|` で区切った指定は、省略しても、順序を入れ替えても構いません。形で種類を判別します。
|
|
65
|
+
|
|
66
|
+
| 形 | 種類 |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `:full`・`:card.html` | スタイル |
|
|
69
|
+
| `+title`・`-title` | ソート |
|
|
70
|
+
| `tags=ruby`・`year>=2020` のように演算子を含むもの | 抽出条件 |
|
|
71
|
+
| 正の整数 | 件数 |
|
|
72
|
+
| それ以外の値 | 主キー検索 |
|
|
73
|
+
|
|
74
|
+
単数形のデータ名に続く最初の指定は、数でも主キー検索として扱います(`= prefecture | 13`)。
|
|
75
|
+
|
|
76
|
+
### 抽出条件
|
|
77
|
+
|
|
78
|
+
| 書き方 | 意味 |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `tags=ruby`・`tags==ruby` | 等しい |
|
|
81
|
+
| `tags=ruby, go` | どちらかに等しい(OR) |
|
|
82
|
+
| `tags!=ruby` | 等しくない |
|
|
83
|
+
| `tags!=ruby, go` | どちらにも等しくない |
|
|
84
|
+
| `year>2020`・`year>=2020` | より大きい・以上 |
|
|
85
|
+
| `year<2020`・`year<=2020` | より小さい・以下 |
|
|
86
|
+
| `year=2020..2024` | 2020 以上 2024 以下 |
|
|
87
|
+
| `year=2020...2024` | 2020 以上 2024 未満 |
|
|
88
|
+
| `year=2020..` | 2020 以上 |
|
|
89
|
+
| `year=..2024` | 2024 以下 |
|
|
90
|
+
| `year=...2024` | 2024 未満 |
|
|
91
|
+
| `tags=ruby AND year>=2020` | 両方を満たす(`and`・`&&` も同じ) |
|
|
92
|
+
| `tags=ruby AND beginner` | `ruby` と `beginner` の両方を含む(2 件目以降の項目名は省ける) |
|
|
93
|
+
| `tags=ruby OR year>=2020` | どちらかを満たす(`or` も同じ) |
|
|
94
|
+
| `tags=ruby OR go` | どちらかを含む(2 件目以降の項目名は省ける) |
|
|
95
|
+
|
|
96
|
+
等しいかどうかを調べるとき、データの値が配列やカンマ区切りの文字列なら、要素のどれかが一致すれば条件を満たします。`tags: [ruby, web]` は `tags=ruby` に一致します。
|
|
97
|
+
|
|
98
|
+
AND は OR より強く結び付きます。`tags=ruby AND year>=2020 OR tags=go` は、「`ruby` で 2020 年以降」または「`go`」を選びます。
|
|
99
|
+
|
|
100
|
+
前後に空白のある `AND`・`and`・`OR`・`or`・`&&` は区切りとして扱うので、値には書けません。`||` は記法の区切りの `|` と重なるので使えません。
|
|
101
|
+
|
|
102
|
+
比較(`>`・`>=`・`<`・`<=`)と範囲では、値が無いレコードと、比較できない値のレコード(数と比べるときの `不明` など)を除きます。
|
|
103
|
+
|
|
104
|
+
### 一件検索
|
|
105
|
+
|
|
106
|
+
値だけを書くと、主キーで検索します。主キーとして調べるのは `id`・`no`・`code`・`slug`・`name`・`title` です。
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
= book | 楽しいRuby # title が一致するレコード
|
|
110
|
+
= post | hello-world # slug が一致するレコード
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
一致するレコードが無ければ `NoResultWarning` を通知し、何も展開しません。複数あれば `AmbiguousQueryWarning` を通知し、一致したレコードをすべて展開します。
|
|
114
|
+
|
|
115
|
+
### ファイルの探し方
|
|
116
|
+
|
|
117
|
+
データファイルは `data_dir` から次の順に探します。拡張子は `.yml`・`.yaml`・`.json` の順に試します。
|
|
118
|
+
|
|
119
|
+
1. 書いたままの名前(`books`)
|
|
120
|
+
2. 末尾に `s` を付けた名前(`book` → `books`)
|
|
121
|
+
3. 単数形にした名前(`books` → `book`)
|
|
122
|
+
|
|
123
|
+
テンプレートは `templates_dir` にある、単数形の名前のファイルを使います。
|
|
124
|
+
|
|
125
|
+
| 記法 | テンプレート |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `= books` | `_book.md` |
|
|
128
|
+
| `= books \| :full` | `_book.full.md` |
|
|
129
|
+
| `= books \| :card.html` | `_book.card.html` |
|
|
130
|
+
|
|
131
|
+
拡張子を書かないときの形式は、`config.default_format` で変更できます。
|
|
132
|
+
|
|
133
|
+
### テンプレートの書き方
|
|
134
|
+
|
|
135
|
+
| 書き方 | 意味 |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `= key`・`=key` | キーの値を展開する |
|
|
138
|
+
| `= author.name` | ネストした値を展開する |
|
|
139
|
+
| ``・`` | 画像のパスをキーの値で展開する |
|
|
140
|
+
| `` | 拡張子のあるパスはそのまま出力する |
|
|
141
|
+
|
|
142
|
+
変数を含む行はレコードごとに繰り返し、含まない行は 1 回だけ出力します。表は次のように書くと、見出しが 1 回だけ出力されます。
|
|
143
|
+
|
|
144
|
+
```markdown
|
|
145
|
+
| 書名 | 著者 |
|
|
146
|
+
|---|---|
|
|
147
|
+
| = title | = author.name |
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
コードブロック(` ``` `)の中の記法は展開しません。
|
|
151
|
+
|
|
152
|
+
### 具体例:Vivlio Starter での書籍データ展開
|
|
47
153
|
|
|
48
154
|
#### データファイル例 (`data/books.yml`)
|
|
49
155
|
|
|
@@ -98,11 +204,55 @@ Rubyを楽しく学べる入門書。
|
|
|
98
204
|
C言語の定番入門書。
|
|
99
205
|
:::
|
|
100
206
|
|
|
207
|
+
|
|
101
208
|
---
|
|
102
209
|
```
|
|
103
210
|
|
|
104
211
|
VFMフェンス記法(`:::{.book-card}`)にも対応しており、各レコードが個別のフェンスで囲まれて展開されます。
|
|
105
212
|
|
|
213
|
+
### 値の無い項目
|
|
214
|
+
|
|
215
|
+
値が無い(キーが無い・空の)項目を含む行は、行ごと出力しません。項目によって情報量の
|
|
216
|
+
違うデータにも、同じテンプレートが使えます。
|
|
217
|
+
|
|
218
|
+
1 行に変数が 2 つ以上ある行では、**値の無い項目の部分だけ**を消します。行は区切り
|
|
219
|
+
(`/`・`・`・`、`・`,`・`,` と、前後に空白のある ` / `)と、変数を含む括弧(`(…)`・`(…)`)で
|
|
220
|
+
部品に分かれ、値の無い変数を含む部品だけが、区切りを詰めて消えます。変数に付いた言葉
|
|
221
|
+
(`著`・`年`)は、変数と一緒に残るか、一緒に消えます。
|
|
222
|
+
|
|
223
|
+
```markdown
|
|
224
|
+
= author.name 著/= publisher/= year 年
|
|
225
|
+
= author.name 著(= year年)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
| データ | 1 行目 | 2 行目 |
|
|
229
|
+
|---|---|---|
|
|
230
|
+
| 全部ある | 高橋征義 著/SBクリエイティブ/2019 年 | 高橋征義 著(2019年) |
|
|
231
|
+
| 発行年が無い | 高橋征義 著/SBクリエイティブ | 高橋征義 著 |
|
|
232
|
+
| 全部無い | (行ごと消える) | (行ごと消える) |
|
|
233
|
+
|
|
234
|
+
- 行頭の箇条書きの `- ` や見出しの `# ` は、先頭の部品が消えても残ります。
|
|
235
|
+
- 表の行(`|` で始まる行)は部品に分けず、値の無い項目があれば行ごと消えます(セルを消すと列がずれるため)。
|
|
236
|
+
- 空白なしの半角の `/` と、リンク先の括弧(`[= title](= url)`)は区切りになりません。
|
|
237
|
+
- テンプレートのキーは、どれか 1 件のデータにあれば正しいキーとみなします。どの件にも無いキーは打ち間違いとして `UnknownKeyError` になります。
|
|
238
|
+
|
|
239
|
+
## メソッド
|
|
240
|
+
|
|
241
|
+
| メソッド | 役割 |
|
|
242
|
+
|---|---|
|
|
243
|
+
| `QueryStream.render(text, **options)` | テキスト内の記法をすべて展開し、結果を返す |
|
|
244
|
+
| `QueryStream.render_query(query, **options)` | 1 つの記法を展開し、結果を返す |
|
|
245
|
+
| `QueryStream.scan(path_or_text)` | ファイルパスまたはテキストから記法を探し、一覧を返す |
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
QueryStream.render_query('= books | tags=ruby | :full', data_dir: 'data', templates_dir: 'templates')
|
|
249
|
+
|
|
250
|
+
QueryStream.scan('contents/05-references.md')
|
|
251
|
+
# => ["= books", "= books | :full"]
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`render_query` は `on_error` を受け取りません。展開に失敗すると、例外をそのまま投げます。`scan` はコードブロックの中の記法を含めません。
|
|
255
|
+
|
|
106
256
|
## 設定
|
|
107
257
|
|
|
108
258
|
```ruby
|
|
@@ -116,7 +266,7 @@ end
|
|
|
116
266
|
|
|
117
267
|
## エラー・警告の扱い
|
|
118
268
|
|
|
119
|
-
**この gem
|
|
269
|
+
**この gem はログを出力しません。** どう表示するか、どの言語で出すか、ログに残すかは、呼び出し元が決めます。そのため展開時の出来事は、構造化された例外としてコールバックへ渡されます。例外の `message` には日本語の既定の文が入っています。
|
|
120
270
|
|
|
121
271
|
```ruby
|
|
122
272
|
QueryStream.render(
|
|
@@ -138,12 +288,13 @@ QueryStream.render(
|
|
|
138
288
|
| `DataNotFoundError` | `expected_path`, `query`, `location` |
|
|
139
289
|
| `DataLoadError` | `file_path`, `cause_error` |
|
|
140
290
|
| `UnknownKeyError` | `key_path`, `available_keys`, `template_path`, `location` |
|
|
291
|
+
| `InvalidDateError` | なし(`message` に日付を含みます) |
|
|
141
292
|
| `NoResultWarning` | `query`, `location` |
|
|
142
293
|
| `AmbiguousQueryWarning` | `query`, `location`, `count` |
|
|
143
294
|
|
|
144
|
-
前
|
|
295
|
+
前 5 つは `QueryStream::Error`、後 2 つは `QueryStream::Warning` を継承します。**gem の外へ出る例外はこの 2 系統だけ**です。壊れた YAML も、不正な UTF-8 を含む原稿も、生の `ArgumentError` や `NoMethodError` にはなりません。
|
|
145
296
|
|
|
146
|
-
たとえば `UnknownKeyError`
|
|
297
|
+
たとえば `UnknownKeyError` は「使えるキーの一覧」と「直すべき雛形のパス」を属性に持つので、呼び出し元で打ち間違いの案内を組み立てられます。`location` は記法が書かれた原稿の位置、`template_path` は直す雛形の位置で、**直すのは雛形のほう**です。
|
|
147
298
|
|
|
148
299
|
```ruby
|
|
149
300
|
on_error: lambda do |e|
|
data/RELEASE_NOTE.md
CHANGED
|
@@ -6,6 +6,45 @@ YAML / JSON のデータファイルとテンプレートを組み合わせて
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## 1.5.0 — 2026-11-01
|
|
10
|
+
|
|
11
|
+
**値の無い項目があっても、行ごと消さずに、その部分だけを消すようになりました。** 書誌のように一行へ複数の値を並べるテンプレートで、どれか一つが欠けると、これまでは行ごと消えていました。
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
= author.name 著/= publisher/= year 年
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
発行年の分からない本でも、`高橋征義 著/SBクリエイティブ` と出ます(1.4 では著者と出版社まで消えていました)。区切り(`/`・`・`・`、` など)と、変数を含む括弧(`(= year 年)`)で行を部品に分け、値の無い部品だけを、区切りを詰めて消します。すべての値がそろった行の出力は変わりません。変数が一つだけの行と表の行は、これまでどおり行ごと消します(表のセルを消すと列がずれるためです)。
|
|
18
|
+
|
|
19
|
+
**抽出条件の書き方を三つ足しました。**
|
|
20
|
+
|
|
21
|
+
- **`OR` で条件をつなげます。** `tags=ruby OR year>=2020` のように、別の項目の条件も書けます。AND は OR より強く結び付きます。前後に空白のある `OR` / `or` は区切りになるので、`title=Rock or Pop` のような値は抽出条件に書けなくなりました
|
|
22
|
+
- **数をアンダースコアで桁区切りして書けます。** `population>=9_000_000` や、万の位で区切る `900_0000` も書けます。比較・範囲・主キー検索・等値のどれでも使えます
|
|
23
|
+
- **日付を和暦(`R06-01-01`)でも書けます。** データでも抽出条件でも、西暦の `2024-01-01` と同じ日として比べ、並べ替えます。元号は頭文字(M・T・S・H・R)で書き、年は 1 桁でも構いません
|
|
24
|
+
|
|
25
|
+
**昇順の並べ替えの書き方を README に追加しました。** 機能は最初からありましたが、README の例は降順(`-title`)だけでした。
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
= books | +title # タイトルの昇順
|
|
29
|
+
= books | -year # 発行年の降順
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**符号は必ず付けてください。** 符号を省いた `= books | title` は、並べ替えではなく**一件検索**として解釈され、「該当なし」の警告を出して何も展開しません。`= book | 楽しいRuby` と同じ形なので、パーサーは符号の有無だけで両者を区別します。README にも理由を記載しました。
|
|
33
|
+
|
|
34
|
+
あわせて、内部に一度も実行されない分岐が残っていたのを整理しました。動きは変わりません。
|
|
35
|
+
|
|
36
|
+
- **対応 Ruby を 3.4.10 / 4.0.9 に更新しました**(4.0.9 は 2026 年 10 月 23 日公開)
|
|
37
|
+
- **Minitest を 6 系へ、samovar を 2.5.1 へ更新しました**
|
|
38
|
+
- **コード検査(rubocop)を導入しました。** 有効にしているのは `Lint` と `Security` の規則だけで、引用符の種類などの様式は検査しません
|
|
39
|
+
- README と CHANGELOG にあった「vivlio-style」という綴りを「Vivlio Starter」に直しました
|
|
40
|
+
|
|
41
|
+
### 不具合の修正
|
|
42
|
+
|
|
43
|
+
- **項目名を省いた AND が OR として動いていたのを直しました。** `tags=ruby AND beginner` は、`ruby` と `beginner` の両方を含むレコードを選ぶ記法ですが、どちらかを含むレコードを選んでいました。この書き方を使っている場合は、結果の件数が変わります
|
|
44
|
+
- **比較とソートで `ArgumentError` が発生していたのを直しました。** 対象の項目に値が無いレコードや比較できない値のレコードが 1 件でもあると、例外が `render` の外へ出ていました。比較では該当のレコードを除き、ソートでは昇順でも降順でも末尾に置きます。`year=2020..abc` のように両端を比較できない範囲も、例外にならなくなりました
|
|
45
|
+
- **省ける項目が最初の 1 件に無いと、`UnknownKeyError` が発生していたのを直しました。** テンプレートのキーを最初の 1 件だけで確認していました。修正後は、どれか 1 件にあるキーを正しいキーとして扱い、どの件にも無いキーだけを `UnknownKeyError` にします
|
|
46
|
+
- **日付の範囲指定が 1 件も返さなかったのを直しました。** `date=2024-06-01..2024-06-30` のような範囲は、両端が文字列のまま比較され、どのデータにも一致しませんでした。数の範囲は影響を受けていません
|
|
47
|
+
|
|
9
48
|
## 1.4.0 — 2026-08-07
|
|
10
49
|
|
|
11
50
|
**細かな不具合修正が中心のリリースです。** いずれも「著者が用意した原稿やデータが壊れている」という、実際に起こりうる場面で想定外の例外が漏れていた問題です。
|
data/lib/query_stream/command.rb
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require 'samovar'
|
|
4
4
|
|
|
5
|
-
# ================================================================
|
|
6
|
-
# File: lib/query_stream/command.rb
|
|
7
5
|
# ================================================================
|
|
8
6
|
# 責務:
|
|
9
7
|
# QueryStream の CLI コマンド。--version のみ提供する。
|
|
@@ -31,11 +29,8 @@ module QueryStream
|
|
|
31
29
|
|
|
32
30
|
# 使い方を表示する。
|
|
33
31
|
#
|
|
34
|
-
# public かつ `output:`
|
|
35
|
-
#
|
|
36
|
-
# 使い方を出そうとする。private に置いていた頃は、そこで
|
|
37
|
-
# `private method 'print_usage' called` の NoMethodError になり、
|
|
38
|
-
# **`--help` と未知の引数がスタックトレースで落ちていた**。
|
|
32
|
+
# public かつ `output:` を受ける形にする。Samovar は引数の解析に失敗すると、外から
|
|
33
|
+
# `error.command.print_usage(output: output)` を呼ぶ。
|
|
39
34
|
# @param output [IO] 出力先(Samovar からはエラー出力が渡される)
|
|
40
35
|
def print_usage(output: $stdout)
|
|
41
36
|
output.puts self.class.description
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
require 'yaml'
|
|
4
4
|
require 'json'
|
|
5
5
|
|
|
6
|
-
# ================================================================
|
|
7
|
-
# File: lib/query_stream/data_resolver.rb
|
|
8
6
|
# ================================================================
|
|
9
7
|
# 責務:
|
|
10
8
|
# データファイルの探索・単数形/複数形の自動解決を行う。
|
|
@@ -97,10 +95,8 @@ module QueryStream
|
|
|
97
95
|
|
|
98
96
|
# 読み込み結果をレコード配列へ正規化する。
|
|
99
97
|
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
# `undefined method 'any?' for nil` で落ちる——データを空にしただけの著者に
|
|
103
|
-
# NoMethodError を見せることになる。
|
|
98
|
+
# **常に配列を返す。** 空のデータファイルは YAML が `nil` を返すので、空配列に変換する
|
|
99
|
+
# (呼び出し先の TemplateCompiler は配列を前提にしている)。
|
|
104
100
|
#
|
|
105
101
|
# 配列でもハッシュでもない中身(先頭が文字列や数値だけの YAML)は、
|
|
106
102
|
# レコード群として解釈しようがないので DataLoadError で理由を伝える。
|
data/lib/query_stream/errors.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# ================================================================
|
|
4
|
-
# File: lib/query_stream/errors.rb
|
|
5
3
|
# ================================================================
|
|
6
4
|
# 責務:
|
|
7
5
|
# QueryStream の例外クラス体系を定義する。
|
|
@@ -55,8 +53,6 @@ module QueryStream
|
|
|
55
53
|
#
|
|
56
54
|
# 「どのキーが無いか」だけでなく「代わりに何が使えるか」「どのファイルを直すか」まで
|
|
57
55
|
# 属性で渡す。呼び出し元はこれを使って「もしかして =author?」のような案内を組み立てられる。
|
|
58
|
-
# 属性が無かった頃はこのクラスだけが gem 内で logger.error を呼んでおり、
|
|
59
|
-
# 利用できるキーの一覧を呼び出し元が受け取れなかった。
|
|
60
56
|
#
|
|
61
57
|
# location(記法の位置)と template_path(雛形の位置)は別物である。
|
|
62
58
|
# 直すのは雛形なので、記法の位置だけでは著者はファイルを開けない。
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require 'date'
|
|
4
4
|
|
|
5
|
-
# ================================================================
|
|
6
|
-
# File: lib/query_stream/filter_engine.rb
|
|
7
5
|
# ================================================================
|
|
8
6
|
# 責務:
|
|
9
7
|
# AND/OR/比較/Range によるフィルタリングとソート処理を行う。
|
|
@@ -13,6 +11,21 @@ require 'date'
|
|
|
13
11
|
module QueryStream
|
|
14
12
|
# フィルタリング&ソートエンジンモジュール
|
|
15
13
|
module FilterEngine
|
|
14
|
+
# 西暦の日付(2024-01-01)
|
|
15
|
+
SEIREKI_DATE = /\A\d{4}-\d{2}-\d{2}\z/
|
|
16
|
+
|
|
17
|
+
# 和暦の日付(R06-01-01)。元号は頭文字で書く(M 明治・T 大正・S 昭和・H 平成・R 令和)。
|
|
18
|
+
# 年は 1 桁でもよい(R6-01-01)。
|
|
19
|
+
WAREKI_DATE = /\A(?<era>[MTSHR])(?<year>\d{1,2})-(?<month>\d{2})-(?<day>\d{2})\z/
|
|
20
|
+
|
|
21
|
+
# 大小の条件と、<=> の結果の判定
|
|
22
|
+
ORDER_TESTS = {
|
|
23
|
+
gt: ->(order) { order.positive? },
|
|
24
|
+
gte: ->(order) { order >= 0 },
|
|
25
|
+
lt: ->(order) { order.negative? },
|
|
26
|
+
lte: ->(order) { order <= 0 }
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
16
29
|
module_function
|
|
17
30
|
|
|
18
31
|
# フィルタ条件をレコード群に適用する
|
|
@@ -27,13 +40,21 @@ module QueryStream
|
|
|
27
40
|
end
|
|
28
41
|
end
|
|
29
42
|
|
|
30
|
-
#
|
|
43
|
+
# ソート条件を適用する。
|
|
44
|
+
# 比べるのは最も多い種類の値で、それ以外の値と、値が無いレコードは、向きによらず末尾に置く。
|
|
31
45
|
# @param records [Array<Hash>] レコード群
|
|
32
46
|
# @param sort [Hash] ソート条件 { field:, direction: }
|
|
33
47
|
# @return [Array<Hash>] ソート後のレコード群
|
|
34
48
|
def apply_sort(records, sort)
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
keyed = records.map do |record|
|
|
50
|
+
value = record[sort[:field]]
|
|
51
|
+
[record, missing_value?(value) ? nil : to_comparable(value)]
|
|
52
|
+
end
|
|
53
|
+
kind = keyed.filter_map { |_, key| sort_kind(key) }.tally.max_by { |_, count| count }&.first
|
|
54
|
+
|
|
55
|
+
sortable, others = keyed.partition { |_, key| sort_kind(key) == kind }
|
|
56
|
+
sorted = sortable.sort_by(&:last).map(&:first)
|
|
57
|
+
(sort[:direction] == :desc ? sorted.reverse : sorted) + others.map(&:first)
|
|
37
58
|
end
|
|
38
59
|
|
|
39
60
|
# 単一フィルタ条件をレコードに対して評価する
|
|
@@ -53,22 +74,55 @@ module QueryStream
|
|
|
53
74
|
match_eq(field_value, filter[:value])
|
|
54
75
|
when :neq
|
|
55
76
|
!match_eq(field_value, filter[:value])
|
|
56
|
-
when :gt
|
|
57
|
-
|
|
58
|
-
when :gte
|
|
59
|
-
to_comparable(field_value) >= to_comparable(filter[:value])
|
|
60
|
-
when :lt
|
|
61
|
-
to_comparable(field_value) < to_comparable(filter[:value])
|
|
62
|
-
when :lte
|
|
63
|
-
to_comparable(field_value) <= to_comparable(filter[:value])
|
|
77
|
+
when :gt, :gte, :lt, :lte
|
|
78
|
+
satisfies_order?(field_value, filter)
|
|
64
79
|
when :range
|
|
65
|
-
|
|
66
|
-
|
|
80
|
+
within_range?(field_value, filter[:value])
|
|
81
|
+
when :or
|
|
82
|
+
filter[:branches].any? { |branch| branch.all? { evaluate_filter(record, it) } }
|
|
67
83
|
else
|
|
68
84
|
false
|
|
69
85
|
end
|
|
70
86
|
end
|
|
71
87
|
|
|
88
|
+
# 大小の条件を満たすか。値が無いレコードと、比較できない値のレコードは満たさない
|
|
89
|
+
# @param field_value [Object] レコードの値
|
|
90
|
+
# @param filter [Hash] フィルタ条件 { op:, value: }
|
|
91
|
+
# @return [Boolean]
|
|
92
|
+
def satisfies_order?(field_value, filter)
|
|
93
|
+
return false if missing_value?(field_value)
|
|
94
|
+
|
|
95
|
+
order = to_comparable(field_value) <=> to_comparable(filter[:value])
|
|
96
|
+
!order.nil? && ORDER_TESTS.fetch(filter[:op]).call(order)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# 範囲に含まれるか。両端も比較用にそろえる。パーサーは "2024-06-01".."2024-06-30" のような
|
|
100
|
+
# 文字列の範囲を返すので、そのままでは Date の値を含まない。
|
|
101
|
+
# @param field_value [Object] レコードの値
|
|
102
|
+
# @param range [Range] 範囲
|
|
103
|
+
# @return [Boolean]
|
|
104
|
+
def within_range?(field_value, range)
|
|
105
|
+
Range.new(to_comparable(range.begin), to_comparable(range.end), range.exclude_end?)
|
|
106
|
+
.cover?(to_comparable(field_value))
|
|
107
|
+
rescue ArgumentError
|
|
108
|
+
# 両端を比較できない範囲(Range.new が受け付けない)
|
|
109
|
+
false
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# 値が無いか(キーが無い・nil・空文字列)
|
|
113
|
+
def missing_value?(value) = value.nil? || value == ''
|
|
114
|
+
|
|
115
|
+
# ソートで比べられる値の種類。値が無いときは nil
|
|
116
|
+
def sort_kind(key)
|
|
117
|
+
case key
|
|
118
|
+
when nil then nil
|
|
119
|
+
when Numeric then :number
|
|
120
|
+
when Date then :date
|
|
121
|
+
when Time then :time
|
|
122
|
+
else :string
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
72
126
|
# 主キー候補フィールドを順番に走査して一致するものを探す
|
|
73
127
|
# @param record [Hash] 単一レコード
|
|
74
128
|
# @param query_value [Object] 検索値
|
|
@@ -82,13 +136,25 @@ module QueryStream
|
|
|
82
136
|
# 等値比較(配列・カンマ区切り文字列を透過的に扱う)
|
|
83
137
|
# データ側が配列/カンマ区切りの場合、ORの値リストと交差判定する
|
|
84
138
|
def match_eq(field_value, filter_values)
|
|
85
|
-
|
|
86
|
-
|
|
139
|
+
# 綴りの揺れは両側でそろえる。条件側だけそろえると、データに "2024_01" や
|
|
140
|
+
# "R06-01-01" のような文字列があるとき、同じ綴りの条件で一致しなくなる。
|
|
141
|
+
field_list = normalize_to_list(field_value).map { canonical_token(it) }
|
|
142
|
+
value_list = Array(filter_values).map { canonical_token(it.to_s.strip) }
|
|
87
143
|
|
|
88
144
|
# フィールド側の値リストと条件値リストに交差があれば一致
|
|
89
145
|
(field_list & value_list).any?
|
|
90
146
|
end
|
|
91
147
|
|
|
148
|
+
# 等値比較のため、値を綴りの揺れのない形へそろえる。
|
|
149
|
+
# 数は桁区切りを外し(9_000_000 → 9000000)、日付は西暦にする(R06-01-01 → 2024-01-01)。
|
|
150
|
+
# 存在しない日付は、等値比較では文字列のまま比べる。
|
|
151
|
+
def canonical_token(str)
|
|
152
|
+
date = parse_date(str)
|
|
153
|
+
date ? date.iso8601 : QueryStreamParser.strip_digit_separators(str)
|
|
154
|
+
rescue InvalidDateError
|
|
155
|
+
str
|
|
156
|
+
end
|
|
157
|
+
|
|
92
158
|
# フィールド値をリスト化する(配列/カンマ区切り/単値を統一)
|
|
93
159
|
def normalize_to_list(value)
|
|
94
160
|
case value
|
|
@@ -103,20 +169,31 @@ module QueryStream
|
|
|
103
169
|
end
|
|
104
170
|
end
|
|
105
171
|
|
|
172
|
+
# 西暦・和暦の日付を Date に直す。日付の形でなければ nil を返す。
|
|
173
|
+
# 和暦は Date.jisx0301 に任せる。Date.parse は "R06-01-01" を 2006-01-01 と
|
|
174
|
+
# 読み違えるので使えない。jisx0301 の区切りはドットで、年は 2 桁が要る。
|
|
175
|
+
# 元号の境目は検査しない(H31-05-01 も 2019-05-01 として読む)。
|
|
176
|
+
# @raise [InvalidDateError] 日付の形だが存在しない日(2024-02-30 など)
|
|
177
|
+
def parse_date(value)
|
|
178
|
+
if value.match?(SEIREKI_DATE)
|
|
179
|
+
Date.parse(value)
|
|
180
|
+
elsif (m = value.match(WAREKI_DATE))
|
|
181
|
+
Date.jisx0301("#{m[:era]}#{m[:year].rjust(2, '0')}.#{m[:month]}.#{m[:day]}")
|
|
182
|
+
end
|
|
183
|
+
rescue Date::Error
|
|
184
|
+
raise InvalidDateError, "無効な日付: #{value}"
|
|
185
|
+
end
|
|
186
|
+
|
|
106
187
|
# 比較用に値を正規化する(数値変換・日付変換を試みる)
|
|
107
188
|
def to_comparable(value)
|
|
108
189
|
case value
|
|
109
190
|
when Date, Time
|
|
110
191
|
value
|
|
111
192
|
when String
|
|
112
|
-
#
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
rescue Date::Error
|
|
117
|
-
raise InvalidDateError, "無効な日付: #{value}"
|
|
118
|
-
end
|
|
119
|
-
end
|
|
193
|
+
# 日付形式(西暦・和暦)を検出して Date に変換
|
|
194
|
+
date = parse_date(value)
|
|
195
|
+
return date if date
|
|
196
|
+
|
|
120
197
|
# 数値変換を試みる
|
|
121
198
|
case value
|
|
122
199
|
when /\A-?\d+\z/
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# ================================================================
|
|
4
|
-
# File: lib/query_stream/query_stream_parser.rb
|
|
5
3
|
# ================================================================
|
|
6
4
|
# 責務:
|
|
7
5
|
# QueryStream 記法(= books | tags=ruby | -title | 5 | :full)を
|
|
@@ -27,6 +25,14 @@ module QueryStream
|
|
|
27
25
|
# AND 条件の区切りパターン
|
|
28
26
|
AND_PATTERN = /\s+(?:AND|and|&&)\s+/
|
|
29
27
|
|
|
28
|
+
# OR 条件の区切りパターン。`||` は記法の区切りの `|` と衝突するので使わない
|
|
29
|
+
OR_PATTERN = /\s+(?:OR|or)\s+/
|
|
30
|
+
|
|
31
|
+
# 桁をアンダースコアで区切った数(9_000_000 / 900_0000 / 1_234.5)。
|
|
32
|
+
# Ruby の数値リテラルと同じく、区切りは数字と数字のあいだにだけ置ける。
|
|
33
|
+
# 区切り方は問わないので、万の位で区切る 900_0000 も書ける。
|
|
34
|
+
NUMBER_WITH_SEPARATORS = /\A-?\d+(?:_\d+)*(?:\.\d+(?:_\d+)*)?\z/
|
|
35
|
+
|
|
30
36
|
module_function
|
|
31
37
|
|
|
32
38
|
# QueryStream 記法をパースして構造化ハッシュを返す
|
|
@@ -137,16 +143,15 @@ module QueryStream
|
|
|
137
143
|
tokens
|
|
138
144
|
end
|
|
139
145
|
|
|
140
|
-
#
|
|
141
|
-
#
|
|
146
|
+
# ソート指定をパースする。
|
|
147
|
+
#
|
|
148
|
+
# **符号は必須である。** 呼び出し側(parse_segment)が `+` か `-` で始まることを確かめてから渡す。
|
|
149
|
+
# 符号なしの `title` は、`= book | 楽しいRuby` と区別できないので主キー検索として扱う。
|
|
150
|
+
#
|
|
151
|
+
# @param token [String] "-title" / "+title"
|
|
142
152
|
# @return [Hash] { field:, direction: }
|
|
143
153
|
def parse_sort(token)
|
|
144
|
-
|
|
145
|
-
in /\A-(.+)\z/
|
|
146
|
-
{ field: $1.to_sym, direction: :desc }
|
|
147
|
-
in /\A\+?(.+)\z/
|
|
148
|
-
{ field: $1.to_sym, direction: :asc }
|
|
149
|
-
end
|
|
154
|
+
{ field: token[1..].to_sym, direction: token.start_with?('-') ? :desc : :asc }
|
|
150
155
|
end
|
|
151
156
|
|
|
152
157
|
# 直前フィールドを引き継いで値のみの条件をパースする
|
|
@@ -157,37 +162,35 @@ module QueryStream
|
|
|
157
162
|
parse_eq_condition(field, value_str)
|
|
158
163
|
end
|
|
159
164
|
|
|
160
|
-
# AND
|
|
161
|
-
#
|
|
162
|
-
# @
|
|
165
|
+
# AND / OR で接続されたフィルタ式をパースする。AND は OR より強く結び付く。
|
|
166
|
+
# 項目名を省いた値は、直前の条件の項目を引き継ぐ(tags=ruby AND beginner)。
|
|
167
|
+
# @param expression [String] "tags=ruby && category=web" / "tags=ruby OR tags=go"
|
|
168
|
+
# @return [Array<Hash>] すべて満たすフィルタ条件のリスト。OR は { op: :or, branches: } の 1 条件にまとめる
|
|
163
169
|
def parse_filter_expression(expression)
|
|
164
|
-
# AND で分割
|
|
165
|
-
clauses = expression.split(AND_PATTERN)
|
|
166
170
|
previous_field = nil
|
|
167
|
-
previous_filter = nil
|
|
168
171
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
+
branches = expression.split(OR_PATTERN).map do |branch|
|
|
173
|
+
branch.split(AND_PATTERN).flat_map do |clause|
|
|
174
|
+
parsed = parse_clause(clause.strip, previous_field)
|
|
175
|
+
previous_field = parsed.last&.[](:field) || previous_field
|
|
176
|
+
parsed
|
|
177
|
+
end
|
|
178
|
+
end.reject(&:empty?)
|
|
172
179
|
|
|
173
|
-
|
|
180
|
+
branches.size > 1 ? [{ op: :or, branches: }] : branches.flatten
|
|
181
|
+
end
|
|
174
182
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
parsed = parse_value_only_condition(previous_field, clause)
|
|
182
|
-
end
|
|
183
|
-
end
|
|
183
|
+
# AND / OR で区切られた 1 節をパースする
|
|
184
|
+
# @param clause [String] "tags=ruby" / "beginner"
|
|
185
|
+
# @param previous_field [Symbol, nil] 項目名を省いたときに引き継ぐ項目
|
|
186
|
+
# @return [Array<Hash>] フィルタ条件
|
|
187
|
+
def parse_clause(clause, previous_field)
|
|
188
|
+
return [] if clause.empty?
|
|
184
189
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
previous_filter = last_filter if last_filter&.[](:op) == :eq
|
|
190
|
+
parsed = parse_single_condition(clause)
|
|
191
|
+
return parsed unless parsed.empty? && previous_field
|
|
188
192
|
|
|
189
|
-
|
|
190
|
-
end
|
|
193
|
+
parse_value_only_condition(previous_field, clause)
|
|
191
194
|
end
|
|
192
195
|
|
|
193
196
|
# 単一条件をパースする(OR はカンマ区切りで表現)
|
|
@@ -242,6 +245,9 @@ module QueryStream
|
|
|
242
245
|
# 通常の等値条件(カンマ区切りでOR)
|
|
243
246
|
[{ field: field_sym, op: :eq, value: parse_values(value_str) }]
|
|
244
247
|
end
|
|
248
|
+
rescue ArgumentError
|
|
249
|
+
# 両端を比較できない範囲(2020..abc)は Range を作れないので、文字列の値として扱う
|
|
250
|
+
[{ field: field_sym, op: :eq, value: parse_values(value_str) }]
|
|
245
251
|
end
|
|
246
252
|
|
|
247
253
|
# カンマ区切りの値をリストとしてパースする
|
|
@@ -255,7 +261,7 @@ module QueryStream
|
|
|
255
261
|
# @param str [String] "20" / "3.14" / "東京"
|
|
256
262
|
# @return [Integer, Float, String] 変換後の値
|
|
257
263
|
def parse_numeric(str)
|
|
258
|
-
s = str.strip
|
|
264
|
+
s = strip_digit_separators(str.strip)
|
|
259
265
|
case s
|
|
260
266
|
when /\A-?\d+\z/ then s.to_i
|
|
261
267
|
when /\A-?\d+\.\d+\z/ then s.to_f
|
|
@@ -263,6 +269,13 @@ module QueryStream
|
|
|
263
269
|
end
|
|
264
270
|
end
|
|
265
271
|
|
|
272
|
+
# 数の桁区切り(アンダースコア)を外す。数でない値(tag_name など)はそのまま返す。
|
|
273
|
+
# @param str [String] "9_000_000" / "ruby"
|
|
274
|
+
# @return [String] "9000000" / "ruby"
|
|
275
|
+
def strip_digit_separators(str)
|
|
276
|
+
str.match?(NUMBER_WITH_SEPARATORS) ? str.delete('_') : str
|
|
277
|
+
end
|
|
278
|
+
|
|
266
279
|
# 主キー候補フィールドによる一件検索フィルタを構築する
|
|
267
280
|
# すべての主キー候補に対して OR 的に検索する
|
|
268
281
|
# @param value [String] 検索値
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# ================================================================
|
|
4
|
-
# File: lib/query_stream/template_compiler.rb
|
|
5
3
|
# ================================================================
|
|
6
4
|
# 責務:
|
|
7
5
|
# テンプレートファイル(_book.md 等)にデータを流し込み、
|
|
@@ -9,6 +7,8 @@
|
|
|
9
7
|
#
|
|
10
8
|
# 変換ルール:
|
|
11
9
|
# - `= key` のみの行 → key の値を展開(nil/空文字なら行スキップ)
|
|
10
|
+
# - `= key` が 2 つ以上ある行 → 区切り(/・、,,)と括弧で部品に分け、
|
|
11
|
+
# 空のキーを含む部品だけを消す(部品が全部消えたら行スキップ。表の行は対象外)
|
|
12
12
|
# - `prefix = key` → prefix を残してkey の値を展開
|
|
13
13
|
# - `` → 変数展開(拡張子なし → 変数、拡張子あり → リテラル)
|
|
14
14
|
# - `= key` を含まない行 → リテラル出力(ヘッダー等は一度だけ出力)
|
|
@@ -44,6 +44,21 @@ module QueryStream
|
|
|
44
44
|
# VFM フェンス終了行: 単独の :::
|
|
45
45
|
FENCE_CLOSE_PATTERN = /\A:::\s*\z/
|
|
46
46
|
|
|
47
|
+
# 部品の区切り: 全角の斜線・中黒・読点・コンマと半角のコンマ(前後の空白ごと)、
|
|
48
|
+
# 前後に空白のある半角の斜線。空白なしの半角の斜線を区切りにしないのは、
|
|
49
|
+
# URL(https://example.com/books/= id)を割らないため。
|
|
50
|
+
PART_SEPARATOR = %r{\s*[/・、,,]\s*|\s+/\s+}
|
|
51
|
+
|
|
52
|
+
# 変数を含みうる括弧(全角・半角)。中身ごと 1 つの部品として扱う(直前の空白も含める)。
|
|
53
|
+
# `]` の直後の括弧は Markdown のリンク先(`[= title](= url)`)なので含めない。
|
|
54
|
+
OPTIONAL_GROUP = /\s*(?<!\])[((][^()()]*[))]/
|
|
55
|
+
|
|
56
|
+
# 行頭の Markdown の目印(箇条書き・番号・引用・見出し)。部品が消えても残す。
|
|
57
|
+
LINE_MARKER = /\A\s*(?:(?:[-*+]|\d+\.)\s+|>\s?|\#{1,6}\s+)*/
|
|
58
|
+
|
|
59
|
+
# 括弧を部品として退避するときの目印(テンプレートに現れない制御文字で挟む)
|
|
60
|
+
GROUP_PLACEHOLDER = /\u0000(\d+)\u0000/
|
|
61
|
+
|
|
47
62
|
module_function
|
|
48
63
|
|
|
49
64
|
# テンプレートにレコード群を流し込んでテキストを生成する
|
|
@@ -55,7 +70,7 @@ module QueryStream
|
|
|
55
70
|
# @return [String] 展開後のテキスト
|
|
56
71
|
def render(template, records, source_filename: nil, line_number: nil, template_path: nil)
|
|
57
72
|
lines = template.lines
|
|
58
|
-
validate_template_keys!(lines, records
|
|
73
|
+
validate_template_keys!(lines, records, source_filename:, line_number:, template_path:) if records.any?
|
|
59
74
|
|
|
60
75
|
parts = classify_lines(lines)
|
|
61
76
|
return '' if records.empty?
|
|
@@ -219,7 +234,7 @@ module QueryStream
|
|
|
219
234
|
end
|
|
220
235
|
|
|
221
236
|
# テンプレート行をレコードデータで展開する
|
|
222
|
-
# nil
|
|
237
|
+
# 行ごと消すときは nil を返す
|
|
223
238
|
# @param line [String] テンプレート行
|
|
224
239
|
# @param record [Hash] データレコード
|
|
225
240
|
# @return [String, nil] 展開後の行、またはスキップ時 nil
|
|
@@ -270,10 +285,72 @@ module QueryStream
|
|
|
270
285
|
end
|
|
271
286
|
|
|
272
287
|
# = key パターンの変数を展開する(ドット記法対応)
|
|
288
|
+
#
|
|
289
|
+
# 変数が 2 つ以上ある行は、空のキーを含む部品だけを消す(expand_parts)。
|
|
290
|
+
# `= author.name 著/= publisher/= year 年` で発行年だけが無い本でも、著者と出版社は出る。
|
|
291
|
+
# 変数が 1 つの行と表の行(セルを消すと列がずれる)は、空なら行ごと消す。
|
|
273
292
|
# @param line [String] テンプレート行
|
|
274
293
|
# @param record [Hash] データレコード
|
|
275
294
|
# @return [String, nil] 展開後の行、またはスキップ時 nil
|
|
276
295
|
def expand_variables(line, record)
|
|
296
|
+
return expand_parts(line, record) if partial_line?(line)
|
|
297
|
+
|
|
298
|
+
expand_whole(line, record)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# 部品ごとに消せる行か。変数が 2 つ以上あり、表の行でないもの。
|
|
302
|
+
def partial_line?(line)
|
|
303
|
+
!line.lstrip.start_with?('|') && line.scan(VARIABLE_PATTERN).size >= 2
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# 区切りと括弧で行を部品に分け、空のキーを含む部品だけを消す。
|
|
307
|
+
#
|
|
308
|
+
# - 変数を含む括弧(`(= year 年)`)は 1 つの部品。空なら直前の空白ごと消す。
|
|
309
|
+
# - 括弧の外は PART_SEPARATOR で分ける。残った部品は、前の部品の後ろにあった区切りでつなぐ。
|
|
310
|
+
# - 行頭の Markdown の目印(`- ` など)と行末の改行は、部品が消えても残す。
|
|
311
|
+
# - 変数を含む部品が 1 つも残らなければ nil(行ごと消す)。
|
|
312
|
+
# キーがすべて埋まっていれば、区切りも括弧も元のまま戻るので、出力は expand_whole と同じになる。
|
|
313
|
+
# @param line [String] テンプレート行
|
|
314
|
+
# @param record [Hash] データレコード
|
|
315
|
+
# @return [String, nil] 展開後の行、またはスキップ時 nil
|
|
316
|
+
def expand_parts(line, record)
|
|
317
|
+
newline = line[/\R\z/].to_s
|
|
318
|
+
body = line.chomp
|
|
319
|
+
marker = body[LINE_MARKER]
|
|
320
|
+
body = body.delete_prefix(marker)
|
|
321
|
+
|
|
322
|
+
groups = []
|
|
323
|
+
masked = body.gsub(OPTIONAL_GROUP) do |group|
|
|
324
|
+
next group unless group.match?(VARIABLE_PATTERN)
|
|
325
|
+
|
|
326
|
+
groups << group
|
|
327
|
+
"\u0000#{groups.size - 1}\u0000"
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
kept = masked.split(/(#{PART_SEPARATOR})/).each_slice(2).filter_map do |part, separator|
|
|
331
|
+
text = expand_part(part, groups, record)
|
|
332
|
+
text && [text, separator.to_s, variable_part?(part)]
|
|
333
|
+
end
|
|
334
|
+
return nil unless kept.any? { |_, _, variable| variable }
|
|
335
|
+
|
|
336
|
+
joined = kept.each_with_index.map { |(text, separator, _), i| i == kept.size - 1 ? text : text + separator }.join
|
|
337
|
+
"#{marker}#{joined}#{newline}"
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# 部品 1 つを展開する。空のキーを含めば nil。括弧は中のキーが空ならそこだけ消す。
|
|
341
|
+
# 括弧の外を先に展開するのは、値の中の `= …` をもう一度展開しないため(括弧は退避中)。
|
|
342
|
+
def expand_part(part, groups, record)
|
|
343
|
+
outer = expand_whole(part, record)
|
|
344
|
+
return nil unless outer
|
|
345
|
+
|
|
346
|
+
text = outer.gsub(GROUP_PLACEHOLDER) { expand_whole(groups[::Regexp.last_match(1).to_i], record).to_s }
|
|
347
|
+
text.strip.empty? ? nil : text
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def variable_part?(part) = part.match?(VARIABLE_PATTERN) || part.match?(GROUP_PLACEHOLDER)
|
|
351
|
+
|
|
352
|
+
# 行の変数をすべて展開する。空のキーが 1 つでもあれば nil(行ごと消す)。
|
|
353
|
+
def expand_whole(line, record)
|
|
277
354
|
result = line.dup
|
|
278
355
|
|
|
279
356
|
result.gsub!(VARIABLE_PATTERN) do |_match|
|
|
@@ -303,16 +380,19 @@ module QueryStream
|
|
|
303
380
|
end
|
|
304
381
|
|
|
305
382
|
# テンプレート内のキーがデータに存在するかを検証する
|
|
383
|
+
#
|
|
384
|
+
# どれか 1 件にでもあるキーは正しいキーとみなす。省ける項目(発行年など)が最初の 1 件に
|
|
385
|
+
# 無いだけで、打ち間違いとして止めないため。値の無い件では、その行・部品を消す(expand_variables)。
|
|
306
386
|
# @param lines [Array<String>] テンプレートの行リスト
|
|
307
|
-
# @param
|
|
387
|
+
# @param records [Array<Hash>] レコード群
|
|
308
388
|
# @param source_filename [String, nil] エラー報告用ファイル名
|
|
309
389
|
# @param line_number [Integer, nil] エラー報告用行番号
|
|
310
390
|
# @param template_path [String, nil] エラー報告用テンプレートパス
|
|
311
|
-
def validate_template_keys!(lines,
|
|
312
|
-
|
|
391
|
+
def validate_template_keys!(lines, records, source_filename: nil, line_number: nil, template_path: nil)
|
|
392
|
+
available_keys = records.flat_map(&:keys).uniq
|
|
393
|
+
return if available_keys.empty?
|
|
313
394
|
|
|
314
395
|
location = source_filename ? "#{source_filename}:#{line_number}" : ''
|
|
315
|
-
available_keys = sample_record.keys
|
|
316
396
|
in_code_block = false
|
|
317
397
|
|
|
318
398
|
lines.each do |line|
|
data/lib/query_stream/version.rb
CHANGED
data/lib/query_stream.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# ================================================================
|
|
4
|
-
# File: lib/query_stream.rb
|
|
5
3
|
# ================================================================
|
|
6
4
|
# 責務:
|
|
7
5
|
# QueryStream gem のエントリポイント。
|
|
@@ -120,8 +118,6 @@ module QueryStream
|
|
|
120
118
|
parsed = QueryStreamParser.parse(query)
|
|
121
119
|
|
|
122
120
|
# --- Phase: Load Data ---
|
|
123
|
-
# gem 内でログ出力せず、構造化された属性を持つ例外を raise する。
|
|
124
|
-
# メッセージの構成・ログ出力・i18n は呼び出し元の責務とする。
|
|
125
121
|
data_file = DataResolver.resolve(parsed[:source], data_dir)
|
|
126
122
|
unless data_file
|
|
127
123
|
expected = File.join(data_dir, "#{parsed[:source]}.yml")
|
|
@@ -147,13 +143,11 @@ module QueryStream
|
|
|
147
143
|
if parsed[:single_lookup]
|
|
148
144
|
case records.size
|
|
149
145
|
when 0
|
|
150
|
-
# gem 内でログ出力せず、構造化された属性を持つ警告を呼び出し元に委ねる。
|
|
151
146
|
on_warning&.call(NoResultWarning.new(query: query, location: location))
|
|
152
147
|
return ''
|
|
153
148
|
when 1
|
|
154
149
|
# 正常
|
|
155
150
|
else
|
|
156
|
-
# gem 内でログ出力せず、構造化された属性を持つ警告を呼び出し元に委ねる。
|
|
157
151
|
on_warning&.call(
|
|
158
152
|
AmbiguousQueryWarning.new(query: query, location: location, count: records.size)
|
|
159
153
|
)
|
|
@@ -168,8 +162,6 @@ module QueryStream
|
|
|
168
162
|
|
|
169
163
|
unless File.exist?(template_path)
|
|
170
164
|
hint = build_template_hint(singular, style, format, templates_dir)
|
|
171
|
-
# gem 内でログ出力せず、構造化された属性を持つ例外を raise する。
|
|
172
|
-
# メッセージの構成・ログ出力・i18n は呼び出し元の責務とする。
|
|
173
165
|
raise TemplateNotFoundError.new(
|
|
174
166
|
template_path: template_path,
|
|
175
167
|
query: query,
|
|
@@ -185,7 +177,7 @@ module QueryStream
|
|
|
185
177
|
|
|
186
178
|
# --- Phase: Post-render filter ---
|
|
187
179
|
# 展開結果を呼び出し元の後段フィルタへ通す(画像パス解決など gem 外の後処理)。
|
|
188
|
-
# コールバックが String
|
|
180
|
+
# コールバックが String 以外を返した場合は、元の展開結果を採用する。
|
|
189
181
|
return rendered unless post_render
|
|
190
182
|
|
|
191
183
|
context = {
|
|
@@ -226,16 +218,12 @@ module QueryStream
|
|
|
226
218
|
|
|
227
219
|
# その行を QueryStream 記法として扱うか。
|
|
228
220
|
#
|
|
229
|
-
#
|
|
230
|
-
# `ArgumentError
|
|
231
|
-
#
|
|
232
|
-
# ここでバイト列を書き換えて(scrub して)通す手もあるが採らない——gem が黙って
|
|
233
|
-
# 原稿のバイト列を変えると、著者が気付けない形で本文が変わる。
|
|
234
|
-
# 記法として読めない行は本文として、そのまま返すのが正しい。
|
|
221
|
+
# 不正な UTF-8 を含む行は記法として解釈せず、本文としてそのまま返す。`match?` はそういう行に
|
|
222
|
+
# `ArgumentError` を投げるので、捕まえないと展開全体が止まる。scrub しないのは、原稿のバイト列を
|
|
223
|
+
# 変えないため。
|
|
235
224
|
#
|
|
236
|
-
# EncodingError と ArgumentError
|
|
237
|
-
#
|
|
238
|
-
# strip は Encoding::CompatibilityError)、その差で展開が止まってはならない。
|
|
225
|
+
# EncodingError と ArgumentError の両方を捕まえる。壊れたバイト列に Ruby が投げる例外はメソッドで
|
|
226
|
+
# 異なる(match? / 先頭破損の lstrip は ArgumentError、strip は Encoding::CompatibilityError)。
|
|
239
227
|
def query_line?(line)
|
|
240
228
|
line.match?(QUERY_STREAM_PATTERN)
|
|
241
229
|
rescue EncodingError, ArgumentError
|
|
@@ -243,9 +231,7 @@ module QueryStream
|
|
|
243
231
|
end
|
|
244
232
|
|
|
245
233
|
# コードフェンスの開始/終了行か。
|
|
246
|
-
# `lstrip` も不正な UTF-8 で ArgumentError を投げるので、query_line?
|
|
247
|
-
# 「読めない行は本文」へ倒す。ここを素通しにすると、記法判定だけ守っても
|
|
248
|
-
# 手前のフェンス判定で落ちる。
|
|
234
|
+
# `lstrip` も不正な UTF-8 で ArgumentError を投げるので、query_line? と同じく本文として扱う。
|
|
249
235
|
def code_fence?(line)
|
|
250
236
|
line.lstrip.start_with?('```')
|
|
251
237
|
rescue EncodingError, ArgumentError
|
|
@@ -254,10 +240,9 @@ module QueryStream
|
|
|
254
240
|
|
|
255
241
|
# 引数をファイルパスとして読むべきか。
|
|
256
242
|
#
|
|
257
|
-
# `scan`
|
|
258
|
-
# `File.
|
|
259
|
-
#
|
|
260
|
-
# パスとして成立しうる形だけを先に選別し、判定自体の失敗も本文扱いに倒す。
|
|
243
|
+
# `scan` はパスと本文のどちらも受け取る。本文を `File.exist?` へ渡すと、NUL バイトを含む場合は
|
|
244
|
+
# `ArgumentError` になり、ディレクトリは `File.read` が `Errno::EISDIR` を出す。
|
|
245
|
+
# パスとして成立する形だけを先に選び、判定の失敗も本文として扱う。
|
|
261
246
|
def readable_file?(str)
|
|
262
247
|
s = str.to_s
|
|
263
248
|
return false if s.empty? || s.include?("\0") || s.include?("\n")
|
data/query-stream.gemspec
CHANGED
|
@@ -8,9 +8,10 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.authors = ['Atelier Mirai']
|
|
9
9
|
spec.email = ['contact@atelier-mirai.net']
|
|
10
10
|
|
|
11
|
-
spec.summary = '
|
|
12
|
-
spec.description = '
|
|
13
|
-
|
|
11
|
+
spec.summary = 'YAML / JSON のデータとテンプレートから、文章中の 1 行の記法を展開する Ruby ライブラリ'
|
|
12
|
+
spec.description = 'QueryStream は、文章中に書いた `= books | tags=ruby | -year | 5` のような 1 行の記法を、' \
|
|
13
|
+
'YAML / JSON のデータファイルとテンプレートを組み合わせて展開する汎用の Ruby ライブラリです。' \
|
|
14
|
+
'抽出条件、並べ替え、件数、スタイルを指定できます。'
|
|
14
15
|
spec.homepage = 'https://github.com/Atelier-Mirai/query-stream'
|
|
15
16
|
spec.license = 'MIT'
|
|
16
17
|
|
|
@@ -20,18 +21,16 @@ Gem::Specification.new do |spec|
|
|
|
20
21
|
spec.required_ruby_version = '>= 3.4'
|
|
21
22
|
|
|
22
23
|
spec.files = Dir.glob('{lib,bin}/**/*') +
|
|
23
|
-
%w[README.md CHANGELOG.md RELEASE_NOTE.md LICENSE
|
|
24
|
+
%w[README.md CHANGELOG.md RELEASE_NOTE.md LICENSE query-stream.gemspec]
|
|
24
25
|
spec.bindir = 'bin'
|
|
25
26
|
spec.executables = ['query-stream']
|
|
26
27
|
spec.require_paths = ['lib']
|
|
27
28
|
|
|
28
|
-
# Runtime dependencies
|
|
29
|
-
# logger への依存は 1.4.0 で外した。gem 自身はログを出さず、失敗は構造化例外で伝える。
|
|
30
29
|
spec.add_dependency 'samovar', '~> 2.1'
|
|
31
30
|
|
|
32
|
-
# Development dependencies
|
|
33
31
|
spec.add_development_dependency 'bundler'
|
|
34
32
|
spec.add_development_dependency 'rake', '~> 13.2'
|
|
35
|
-
spec.add_development_dependency 'minitest', '
|
|
33
|
+
spec.add_development_dependency 'minitest', '>= 5.22'
|
|
34
|
+
spec.add_development_dependency 'rubocop', '~> 1.81'
|
|
36
35
|
spec.metadata['rubygems_mfa_required'] = 'false'
|
|
37
36
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: query-stream
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Atelier Mirai
|
|
@@ -55,18 +55,32 @@ dependencies:
|
|
|
55
55
|
name: minitest
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
57
57
|
requirements:
|
|
58
|
-
- - "
|
|
58
|
+
- - ">="
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
60
|
version: '5.22'
|
|
61
61
|
type: :development
|
|
62
62
|
prerelease: false
|
|
63
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
64
|
requirements:
|
|
65
|
-
- - "
|
|
65
|
+
- - ">="
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
67
|
version: '5.22'
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rubocop
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '1.81'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '1.81'
|
|
82
|
+
description: QueryStream は、文章中に書いた `= books | tags=ruby | -year | 5` のような 1 行の記法を、YAML
|
|
83
|
+
/ JSON のデータファイルとテンプレートを組み合わせて展開する汎用の Ruby ライブラリです。抽出条件、並べ替え、件数、スタイルを指定できます。
|
|
70
84
|
email:
|
|
71
85
|
- contact@atelier-mirai.net
|
|
72
86
|
executables:
|
|
@@ -75,7 +89,6 @@ extensions: []
|
|
|
75
89
|
extra_rdoc_files: []
|
|
76
90
|
files:
|
|
77
91
|
- CHANGELOG.md
|
|
78
|
-
- Gemfile
|
|
79
92
|
- LICENSE
|
|
80
93
|
- README.md
|
|
81
94
|
- RELEASE_NOTE.md
|
|
@@ -112,7 +125,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
112
125
|
- !ruby/object:Gem::Version
|
|
113
126
|
version: '0'
|
|
114
127
|
requirements: []
|
|
115
|
-
rubygems_version: 4.0.
|
|
128
|
+
rubygems_version: 4.0.20
|
|
116
129
|
specification_version: 4
|
|
117
|
-
summary:
|
|
130
|
+
summary: YAML / JSON のデータとテンプレートから、文章中の 1 行の記法を展開する Ruby ライブラリ
|
|
118
131
|
test_files: []
|