query-stream 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cde26fe750e8ad95270ff7d757ace2e9401fffa40d1d4bfaef36e090dcff9e5c
4
+ data.tar.gz: 47628d365441535243d08a4ac109ce8e102e1401a5c14e6d31519f39948d1707
5
+ SHA512:
6
+ metadata.gz: 6b795dcb38ab09d478ebab374e129d7121ff7a15e7fabba554629278cd94b2f47dda90af400f8b9a42eb87df99aa632af01edf92b3238fc1efeebe0f9ade3a3e
7
+ data.tar.gz: fb34d776cde2a2f7665c22bb22c446687543ee104591bee2e712dabe572c0f4b3652b8a215ce5f7890ca581705e452317510c6f256c42c2d69e65c2efd359ae8
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,41 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Atelier Mirai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ # 日本語訳(参考)
26
+
27
+ MIT ライセンス
28
+
29
+ Copyright (c) 2025 Atelier Mirai
30
+
31
+ 本ソフトウェアおよび関連ドキュメントファイル(以下「ソフトウェア」)の複製を取得したすべての人に対して、
32
+ ソフトウェアを制限なく扱うことを無償で許可します。これには、ソフトウェアの複製を使用、複製、変更、
33
+ 結合、公表、頒布、サブライセンス、および/または販売する権利、およびソフトウェアを提供された人が
34
+ そうすることを許可する権利も含まれますが、それらに限定されません。
35
+
36
+ 上記の著作権表示およびこの許可表示を、ソフトウェアのすべての複製または重要な部分に含めるものとします。
37
+
38
+ 本ソフトウェアは「現状のまま」提供され、明示または黙示を問わず、商品性、特定目的への適合性、
39
+ および権利の非侵害を含むがそれらに限定されないいかなる保証もありません。
40
+ 著作権者または権利者は、ソフトウェアに関して、契約、不法行為、またはその他の方法で
41
+ 発生するいかなる請求、損害、責任についても責任を負いません。
data/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # QueryStream
2
+
3
+ YAML/JSON データファイルとテンプレートファイルを組み合わせて、テキストコンテンツ内の QueryStream 記法を展開する汎用 Ruby ライブラリ。
4
+
5
+ ## インストール
6
+
7
+ ```ruby
8
+ # Gemfile
9
+ gem 'query-stream'
10
+ ```
11
+
12
+ ```bash
13
+ bundle install
14
+ ```
15
+
16
+ ## 基本的な使い方
17
+
18
+ ```ruby
19
+ require 'query_stream'
20
+
21
+ # テキスト内の QueryStream 記法を展開
22
+ source = File.read('contents/05-references.md')
23
+ result = QueryStream.render(source)
24
+ ```
25
+
26
+ ## QueryStream 記法
27
+
28
+ ```
29
+ = [源泉] | [抽出条件] | [ソート] | [件数] | [スタイル]
30
+ ```
31
+
32
+ ### 例
33
+
34
+ ```
35
+ = books # 全件展開
36
+ = books | tags=ruby # タグで絞り込み
37
+ = books | tags=ruby | -title | 5 # 絞り込み+降順ソート+5件
38
+ = book | 楽しいRuby # 主キーで一件検索
39
+ = books | :full # fullスタイルで展開
40
+ ```
41
+
42
+ ### 具体例:vivlio-style での書籍データ展開
43
+
44
+ #### データファイル例 (`data/books.yml`)
45
+
46
+ ```yaml
47
+ - title: 楽しいRuby
48
+ author:
49
+ name: 高橋征義
50
+ desc: Rubyを楽しく学べる入門書。
51
+ cover: ruby.webp
52
+
53
+ - title: はじめてのC
54
+ author:
55
+ name: 柴田望洋
56
+ desc: C言語の定番入門書。
57
+ cover: c.webp
58
+ ```
59
+
60
+ #### テンプレートファイル例 (`templates/_book.md`)
61
+
62
+ ```markdown
63
+ :::{.book-card}
64
+ ![](cover)
65
+ **=title**
66
+ =desc
67
+ :::
68
+ ```
69
+
70
+ #### Markdown記述
71
+
72
+ ```markdown
73
+ ## 参考書籍
74
+
75
+ = books
76
+
77
+ ---
78
+ ```
79
+
80
+ #### 展開結果
81
+
82
+ ```markdown
83
+ ## 参考書籍
84
+
85
+ :::{.book-card}
86
+ ![](ruby.webp)
87
+ **楽しいRuby**
88
+ Rubyを楽しく学べる入門書。
89
+ :::
90
+
91
+ :::{.book-card}
92
+ ![](c.webp)
93
+ **はじめてのC**
94
+ C言語の定番入門書。
95
+ :::
96
+
97
+ ---
98
+ ```
99
+
100
+ VFMフェンス記法(`:::{.book-card}`)にも対応しており、各レコードが個別のフェンスで囲まれて展開されます。
101
+
102
+ ## 設定
103
+
104
+ ```ruby
105
+ QueryStream.configure do |config|
106
+ config.data_dir = 'data'
107
+ config.templates_dir = 'templates'
108
+ config.default_format = :md
109
+ config.logger = Logger.new($stdout)
110
+ end
111
+ ```
112
+
113
+ ## ライセンス
114
+
115
+ MIT License
data/bin/query-stream ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'query_stream'
5
+ require 'query_stream/command'
6
+
7
+ QueryStream::Command::Top.call(ARGV)
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'samovar'
4
+
5
+ # ================================================================
6
+ # File: lib/query_stream/command.rb
7
+ # ================================================================
8
+ # 責務:
9
+ # QueryStream の CLI コマンド。--version のみ提供する。
10
+ # ================================================================
11
+
12
+ module QueryStream
13
+ module Command
14
+ # トップレベルコマンド
15
+ class Top < Samovar::Command
16
+ self.description = 'QueryStream - YAML/JSON data renderer'
17
+
18
+ options do
19
+ option '--version', 'Print version and exit'
20
+ end
21
+
22
+ # コマンド実行
23
+ def call
24
+ if @options[:version]
25
+ puts "query-stream #{QueryStream::VERSION}"
26
+ else
27
+ print_usage
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def print_usage
34
+ puts self.class.description
35
+ puts "Usage: query-stream [options]"
36
+ puts " --version Print version and exit"
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ # ================================================================
6
+ # File: lib/query_stream/configuration.rb
7
+ # ================================================================
8
+ # 責務:
9
+ # QueryStream のグローバル設定を管理する。
10
+ # data_dir, templates_dir, default_format, logger の4項目。
11
+ # ================================================================
12
+
13
+ module QueryStream
14
+ # グローバル設定クラス
15
+ class Configuration
16
+ # @return [String] データファイルのディレクトリ
17
+ attr_accessor :data_dir
18
+
19
+ # @return [String] テンプレートファイルのディレクトリ
20
+ attr_accessor :templates_dir
21
+
22
+ # @return [Symbol] スタイル省略時のデフォルト出力形式(:md / :html / :json)
23
+ attr_accessor :default_format
24
+
25
+ # @return [Logger] ログ出力先
26
+ attr_accessor :logger
27
+
28
+ def initialize
29
+ @data_dir = 'data'
30
+ @templates_dir = 'templates'
31
+ @default_format = :md
32
+ @logger = Logger.new($stdout)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'json'
5
+
6
+ # ================================================================
7
+ # File: lib/query_stream/data_resolver.rb
8
+ # ================================================================
9
+ # 責務:
10
+ # データファイルの探索・単数形/複数形の自動解決を行う。
11
+ # YAML (.yml, .yaml) と JSON (.json) をサポートする。
12
+ # ================================================================
13
+
14
+ module QueryStream
15
+ # データファイル探索・読み込みモジュール
16
+ module DataResolver
17
+ # サポートする拡張子(優先順位順)
18
+ EXTENSIONS = %w[.yml .yaml .json].freeze
19
+
20
+ module_function
21
+
22
+ # データファイルのパスを解決する
23
+ # 指定名そのまま → 複数形(末尾に s を付与)→ 単数形の順で探索する
24
+ # @param source_name [String] データ名(単数形または複数形)
25
+ # @param data_dir [String] データディレクトリ
26
+ # @return [String, nil] 見つかったファイルパス、または nil
27
+ def resolve(source_name, data_dir)
28
+ # そのまま試行
29
+ found = find_with_extensions(source_name, data_dir)
30
+ return found if found
31
+
32
+ # 複数形を試行(単数形→複数形: book → books)
33
+ found = find_with_extensions("#{source_name}s", data_dir)
34
+ return found if found
35
+
36
+ # 単数形を試行(複数形→単数形: books → book)
37
+ singular = Singularize.call(source_name)
38
+ if singular != source_name
39
+ found = find_with_extensions(singular, data_dir)
40
+ return found if found
41
+ end
42
+
43
+ nil
44
+ end
45
+
46
+ # レコード群をファイルから読み込む
47
+ # @param file_path [String] データファイルパス
48
+ # @return [Array<Hash>] レコード群(シンボルキー)
49
+ def load_records(file_path)
50
+ records = case File.extname(file_path).downcase
51
+ when '.json'
52
+ JSON.parse(File.read(file_path, encoding: 'utf-8'), symbolize_names: true)
53
+ else
54
+ YAML.load_file(file_path, symbolize_names: true)
55
+ end
56
+
57
+ records = [records] if records.is_a?(Hash)
58
+ records
59
+ end
60
+
61
+ # 指定名ですべての拡張子を試行する
62
+ # @param base_name [String] 拡張子なしファイル名
63
+ # @param data_dir [String] データディレクトリ
64
+ # @return [String, nil] 見つかったファイルパス、または nil
65
+ def find_with_extensions(base_name, data_dir)
66
+ EXTENSIONS.each do |ext|
67
+ path = File.join(data_dir, "#{base_name}#{ext}")
68
+ return path if File.exist?(path)
69
+ end
70
+ nil
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ================================================================
4
+ # File: lib/query_stream/errors.rb
5
+ # ================================================================
6
+ # 責務:
7
+ # QueryStream の例外クラス体系を定義する。
8
+ # ERROR系は処理を中断し、WARNING系は処理を続行する。
9
+ # ================================================================
10
+
11
+ module QueryStream
12
+ # 基底クラス
13
+ class Error < StandardError; end
14
+ class Warning < StandardError; end
15
+
16
+ # ERROR系(処理を中断)
17
+ class TemplateNotFoundError < Error; end # テンプレートファイルが存在しない
18
+ class DataNotFoundError < Error; end # データファイルが存在しない
19
+ class UnknownKeyError < Error; end # テンプレート内に存在しないキー
20
+ class InvalidDateError < Error; end # 無効な日付
21
+
22
+ # WARNING系(処理を続行)
23
+ class AmbiguousQueryWarning < Warning; end # 一件検索で複数件ヒット
24
+ class NoResultWarning < Warning; end # 一件検索で0件ヒット
25
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ # ================================================================
6
+ # File: lib/query_stream/filter_engine.rb
7
+ # ================================================================
8
+ # 責務:
9
+ # AND/OR/比較/Range によるフィルタリングとソート処理を行う。
10
+ # 日付の自動正規化にも対応する。
11
+ # ================================================================
12
+
13
+ module QueryStream
14
+ # フィルタリング&ソートエンジンモジュール
15
+ module FilterEngine
16
+ module_function
17
+
18
+ # フィルタ条件をレコード群に適用する
19
+ # @param records [Array<Hash>] レコード群
20
+ # @param filters [Array<Hash>] フィルタ条件の配列
21
+ # @return [Array<Hash>] フィルタ後のレコード群
22
+ def apply_filters(records, filters)
23
+ return records if filters.nil? || filters.empty?
24
+
25
+ records.select do |record|
26
+ filters.all? { evaluate_filter(record, it) }
27
+ end
28
+ end
29
+
30
+ # ソート条件を適用する
31
+ # @param records [Array<Hash>] レコード群
32
+ # @param sort [Hash] ソート条件 { field:, direction: }
33
+ # @return [Array<Hash>] ソート後のレコード群
34
+ def apply_sort(records, sort)
35
+ sorted = records.sort_by { to_comparable(it[sort[:field]]) }
36
+ sort[:direction] == :desc ? sorted.reverse : sorted
37
+ end
38
+
39
+ # 単一フィルタ条件をレコードに対して評価する
40
+ # @param record [Hash] 単一レコード
41
+ # @param filter [Hash] フィルタ条件 { field:, op:, value: }
42
+ # @return [Boolean] 条件に合致するか
43
+ def evaluate_filter(record, filter)
44
+ # 主キー検索(_primary_key)の特別処理
45
+ if filter[:field] == :_primary_key
46
+ return evaluate_primary_key_lookup(record, filter[:value])
47
+ end
48
+
49
+ field_value = record[filter[:field]]
50
+
51
+ case filter[:op]
52
+ when :eq
53
+ match_eq(field_value, filter[:value])
54
+ when :neq
55
+ !match_eq(field_value, filter[:value])
56
+ when :gt
57
+ to_comparable(field_value) > to_comparable(filter[:value])
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])
64
+ when :range
65
+ range = filter[:value]
66
+ range.cover?(to_comparable(field_value))
67
+ else
68
+ false
69
+ end
70
+ end
71
+
72
+ # 主キー候補フィールドを順番に走査して一致するものを探す
73
+ # @param record [Hash] 単一レコード
74
+ # @param query_value [Object] 検索値
75
+ # @return [Boolean] いずれかの主キー候補と一致するか
76
+ def evaluate_primary_key_lookup(record, query_value)
77
+ QueryStreamParser::PRIMARY_KEY_FIELDS.any? do |key|
78
+ record[key]&.to_s == query_value.to_s
79
+ end
80
+ end
81
+
82
+ # 等値比較(配列・カンマ区切り文字列を透過的に扱う)
83
+ # データ側が配列/カンマ区切りの場合、ORの値リストと交差判定する
84
+ def match_eq(field_value, filter_values)
85
+ field_list = normalize_to_list(field_value)
86
+ value_list = Array(filter_values).map { it.to_s.strip }
87
+
88
+ # フィールド側の値リストと条件値リストに交差があれば一致
89
+ (field_list & value_list).any?
90
+ end
91
+
92
+ # フィールド値をリスト化する(配列/カンマ区切り/単値を統一)
93
+ def normalize_to_list(value)
94
+ case value
95
+ when Array
96
+ value.map { it.to_s.strip }
97
+ when String
98
+ value.split(',').map { it.strip }
99
+ when nil
100
+ []
101
+ else
102
+ [value.to_s.strip]
103
+ end
104
+ end
105
+
106
+ # 比較用に値を正規化する(数値変換・日付変換を試みる)
107
+ def to_comparable(value)
108
+ case value
109
+ when Date, Time
110
+ value
111
+ when String
112
+ # 日付形式(YYYY-MM-DD)を検出して Date に変換
113
+ if value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
114
+ begin
115
+ return Date.parse(value)
116
+ rescue Date::Error
117
+ raise InvalidDateError, "無効な日付: #{value}"
118
+ end
119
+ end
120
+ # 数値変換を試みる
121
+ case value
122
+ when /\A-?\d+\z/
123
+ value.to_i
124
+ when /\A-?\d+\.\d+\z/
125
+ value.to_f
126
+ else
127
+ value
128
+ end
129
+ when Integer, Float
130
+ value
131
+ else
132
+ value.to_s
133
+ end
134
+ end
135
+ end
136
+ end