query-stream 1.1.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 +7 -0
- data/Gemfile +5 -0
- data/LICENSE +41 -0
- data/README.md +115 -0
- data/bin/query-stream +7 -0
- data/lib/query_stream/command.rb +40 -0
- data/lib/query_stream/configuration.rb +35 -0
- data/lib/query_stream/data_resolver.rb +73 -0
- data/lib/query_stream/errors.rb +60 -0
- data/lib/query_stream/filter_engine.rb +136 -0
- data/lib/query_stream/query_stream_parser.rb +280 -0
- data/lib/query_stream/singularize.rb +39 -0
- data/lib/query_stream/template_compiler.rb +330 -0
- data/lib/query_stream/version.rb +5 -0
- data/lib/query_stream.rb +240 -0
- data/query-stream.gemspec +36 -0
- metadata +130 -0
data/lib/query_stream.rb
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# ================================================================
|
|
4
|
+
# File: lib/query_stream.rb
|
|
5
|
+
# ================================================================
|
|
6
|
+
# 責務:
|
|
7
|
+
# QueryStream gem のエントリポイント。
|
|
8
|
+
# YAML/JSON データファイルとテンプレートファイルを組み合わせて、
|
|
9
|
+
# テキストコンテンツ内の QueryStream 記法を展開する汎用ライブラリ。
|
|
10
|
+
#
|
|
11
|
+
# 公開 API:
|
|
12
|
+
# QueryStream.render(source, **options) - テキスト内の記法をすべて展開
|
|
13
|
+
# QueryStream.render_query(query, **options) - 単一の QueryStream 記法を展開
|
|
14
|
+
# QueryStream.scan(path_or_content) - 記法を検出してリストを返す
|
|
15
|
+
# QueryStream.configure { |config| ... } - 設定
|
|
16
|
+
# ================================================================
|
|
17
|
+
|
|
18
|
+
require_relative 'query_stream/version'
|
|
19
|
+
require_relative 'query_stream/errors'
|
|
20
|
+
require_relative 'query_stream/configuration'
|
|
21
|
+
require_relative 'query_stream/singularize'
|
|
22
|
+
require_relative 'query_stream/query_stream_parser'
|
|
23
|
+
require_relative 'query_stream/template_compiler'
|
|
24
|
+
require_relative 'query_stream/data_resolver'
|
|
25
|
+
require_relative 'query_stream/filter_engine'
|
|
26
|
+
|
|
27
|
+
module QueryStream
|
|
28
|
+
# QueryStream 記法を検出する正規表現
|
|
29
|
+
# 行頭 = の直後に英数字/ハイフン/アンダースコアのデータ名(スペースは任意)
|
|
30
|
+
QUERY_STREAM_PATTERN = /^=\s*([a-zA-Z][a-zA-Z0-9_-]*)(?:\s*\|.*)?$/
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
# グローバル設定を返す
|
|
34
|
+
# @return [Configuration]
|
|
35
|
+
def configuration
|
|
36
|
+
@configuration ||= Configuration.new
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# 設定をブロックで変更する
|
|
40
|
+
# @yield [Configuration]
|
|
41
|
+
def configure
|
|
42
|
+
yield(configuration)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# ロガーへのショートカット
|
|
46
|
+
# @return [Logger]
|
|
47
|
+
def logger
|
|
48
|
+
configuration.logger
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# テキストコンテンツ内の QueryStream 記法をすべて展開する
|
|
52
|
+
# 1行の展開に失敗しても残りの行の処理を継続する。
|
|
53
|
+
# エラー情報は例外の属性として呼び出し元に委ねる(gem 内ではログ出力しない)。
|
|
54
|
+
# @param content [String] テキストコンテンツ
|
|
55
|
+
# @param source_filename [String, nil] エラー報告用のソースファイル名
|
|
56
|
+
# @param data_dir [String, nil] データディレクトリ(nil時はconfigを使用)
|
|
57
|
+
# @param templates_dir [String, nil] テンプレートディレクトリ(nil時はconfigを使用)
|
|
58
|
+
# @param on_error [Proc, nil] エラー時コールバック。|exception| を受け取る。省略時は何もしない。
|
|
59
|
+
# @return [String] 展開後のテキストコンテンツ
|
|
60
|
+
def render(content, source_filename: nil, data_dir: nil, templates_dir: nil, on_error: nil)
|
|
61
|
+
data_dir ||= configuration.data_dir
|
|
62
|
+
templates_dir ||= configuration.templates_dir
|
|
63
|
+
|
|
64
|
+
lines = content.lines
|
|
65
|
+
result = []
|
|
66
|
+
in_code_block = false
|
|
67
|
+
|
|
68
|
+
lines.each_with_index do |line, idx|
|
|
69
|
+
line_number = idx + 1
|
|
70
|
+
|
|
71
|
+
# コードブロック内はスキップ
|
|
72
|
+
if line.lstrip.start_with?('```')
|
|
73
|
+
in_code_block = !in_code_block
|
|
74
|
+
result << line
|
|
75
|
+
next
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if in_code_block
|
|
79
|
+
result << line
|
|
80
|
+
next
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# QueryStream 記法の検出
|
|
84
|
+
if line.match?(QUERY_STREAM_PATTERN)
|
|
85
|
+
begin
|
|
86
|
+
expanded = render_query(
|
|
87
|
+
line.chomp, line_number:, source_filename:, data_dir:, templates_dir:
|
|
88
|
+
)
|
|
89
|
+
result << expanded << "\n"
|
|
90
|
+
rescue Error => e
|
|
91
|
+
# 1行の失敗で残りの展開を止めない。エラー処理は呼び出し元に委ねる。
|
|
92
|
+
on_error&.call(e)
|
|
93
|
+
result << line
|
|
94
|
+
end
|
|
95
|
+
else
|
|
96
|
+
result << line
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
result.join
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# 単一の QueryStream 記法を展開する
|
|
104
|
+
# @param query [String] QueryStream 記法の行(例: "= books | tags=ruby | :full")
|
|
105
|
+
# @param line_number [Integer, nil] 行番号(エラー報告用)
|
|
106
|
+
# @param source_filename [String, nil] ソースファイル名
|
|
107
|
+
# @param data_dir [String, nil] データディレクトリ
|
|
108
|
+
# @param templates_dir [String, nil] テンプレートディレクトリ
|
|
109
|
+
# @return [String] 展開後のテキスト
|
|
110
|
+
def render_query(query, line_number: nil, source_filename: nil, data_dir: nil, templates_dir: nil)
|
|
111
|
+
data_dir ||= configuration.data_dir
|
|
112
|
+
templates_dir ||= configuration.templates_dir
|
|
113
|
+
location = source_filename ? "#{source_filename}:#{line_number}" : "行#{line_number}"
|
|
114
|
+
|
|
115
|
+
# --- Phase: Parse ---
|
|
116
|
+
parsed = QueryStreamParser.parse(query)
|
|
117
|
+
|
|
118
|
+
# --- Phase: Load Data ---
|
|
119
|
+
# gem 内でログ出力せず、構造化された属性を持つ例外を raise する。
|
|
120
|
+
# メッセージの構成・ログ出力・i18n は呼び出し元の責務とする。
|
|
121
|
+
data_file = DataResolver.resolve(parsed[:source], data_dir)
|
|
122
|
+
unless data_file
|
|
123
|
+
expected = File.join(data_dir, "#{parsed[:source]}.yml")
|
|
124
|
+
raise DataNotFoundError.new(
|
|
125
|
+
expected_path: expected,
|
|
126
|
+
query: query,
|
|
127
|
+
location: location
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
records = DataResolver.load_records(data_file)
|
|
132
|
+
|
|
133
|
+
# --- Phase: Filter ---
|
|
134
|
+
records = FilterEngine.apply_filters(records, parsed[:filters])
|
|
135
|
+
|
|
136
|
+
# --- Phase: Sort ---
|
|
137
|
+
records = FilterEngine.apply_sort(records, parsed[:sort]) if parsed[:sort]
|
|
138
|
+
|
|
139
|
+
# --- Phase: Limit ---
|
|
140
|
+
records = records.first(parsed[:limit]) if parsed[:limit]
|
|
141
|
+
|
|
142
|
+
# --- Phase: Single record warning ---
|
|
143
|
+
if parsed[:single_lookup]
|
|
144
|
+
case records.size
|
|
145
|
+
when 0
|
|
146
|
+
logger.warn("一件検索で該当なし(#{location}): #{query}")
|
|
147
|
+
return ''
|
|
148
|
+
when 1
|
|
149
|
+
# 正常
|
|
150
|
+
else
|
|
151
|
+
logger.warn("一件検索で複数件ヒット(#{location}): #{query}")
|
|
152
|
+
logger.warn(" #{records.size}件見つかりました。条件を明示してください。")
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# --- Phase: Resolve Template ---
|
|
157
|
+
singular = Singularize.call(parsed[:source])
|
|
158
|
+
style = parsed[:style]
|
|
159
|
+
format = parsed[:format]
|
|
160
|
+
template_path = resolve_template_path(singular, style, format, templates_dir)
|
|
161
|
+
|
|
162
|
+
unless File.exist?(template_path)
|
|
163
|
+
hint = build_template_hint(singular, style, format, templates_dir)
|
|
164
|
+
# gem 内でログ出力せず、構造化された属性を持つ例外を raise する。
|
|
165
|
+
# メッセージの構成・ログ出力・i18n は呼び出し元の責務とする。
|
|
166
|
+
raise TemplateNotFoundError.new(
|
|
167
|
+
template_path: template_path,
|
|
168
|
+
query: query,
|
|
169
|
+
location: location,
|
|
170
|
+
hint: hint
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
template_content = File.read(template_path, encoding: 'utf-8')
|
|
175
|
+
|
|
176
|
+
# --- Phase: Render ---
|
|
177
|
+
TemplateCompiler.render(template_content, records, source_filename:, line_number:)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# テキスト内の QueryStream 記法を検出してリストを返す
|
|
181
|
+
# @param path_or_content [String] ファイルパスまたはテキストコンテンツ
|
|
182
|
+
# @return [Array<String>] 検出された QueryStream 記法のリスト
|
|
183
|
+
def scan(path_or_content)
|
|
184
|
+
content = File.exist?(path_or_content) ? File.read(path_or_content, encoding: 'utf-8') : path_or_content
|
|
185
|
+
lines = content.lines
|
|
186
|
+
queries = []
|
|
187
|
+
in_code_block = false
|
|
188
|
+
|
|
189
|
+
lines.each do |line|
|
|
190
|
+
if line.lstrip.start_with?('```')
|
|
191
|
+
in_code_block = !in_code_block
|
|
192
|
+
next
|
|
193
|
+
end
|
|
194
|
+
next if in_code_block
|
|
195
|
+
|
|
196
|
+
queries << line.chomp if line.match?(QUERY_STREAM_PATTERN)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
queries
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private
|
|
203
|
+
|
|
204
|
+
# テンプレートファイルパスを解決する
|
|
205
|
+
# @param singular_name [String] 単数形のデータ名
|
|
206
|
+
# @param style [String, nil] スタイル名
|
|
207
|
+
# @param format [String, nil] 出力形式(拡張子)
|
|
208
|
+
# @param templates_dir [String] テンプレートディレクトリ
|
|
209
|
+
# @return [String] テンプレートファイルパス
|
|
210
|
+
def resolve_template_path(singular_name, style, format, templates_dir)
|
|
211
|
+
ext = format || configuration.default_format.to_s
|
|
212
|
+
ext = 'md' if ext == 'md' || ext.empty?
|
|
213
|
+
|
|
214
|
+
if style
|
|
215
|
+
# :table.html → _book.table.html
|
|
216
|
+
# :full → _book.full.md
|
|
217
|
+
if format
|
|
218
|
+
File.join(templates_dir, "_#{singular_name}.#{style}.#{format}")
|
|
219
|
+
else
|
|
220
|
+
File.join(templates_dir, "_#{singular_name}.#{style}.#{ext}")
|
|
221
|
+
end
|
|
222
|
+
else
|
|
223
|
+
File.join(templates_dir, "_#{singular_name}.#{ext}")
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# テンプレート不在時のヒントメッセージを生成する
|
|
228
|
+
def build_template_hint(singular_name, style, format, templates_dir)
|
|
229
|
+
default_ext = format || configuration.default_format.to_s
|
|
230
|
+
default_ext = 'md' if default_ext == 'md' || default_ext.empty?
|
|
231
|
+
default_path = File.join(templates_dir, "_#{singular_name}.#{default_ext}")
|
|
232
|
+
|
|
233
|
+
if style && File.exist?(default_path)
|
|
234
|
+
"#{default_path} は存在します。スタイル名を確認してください。"
|
|
235
|
+
else
|
|
236
|
+
nil
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/query_stream/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'query-stream'
|
|
7
|
+
spec.version = QueryStream::VERSION
|
|
8
|
+
spec.authors = ['Atelier Mirai']
|
|
9
|
+
spec.email = ['contact@atelier-mirai.net']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'QueryStream - YAML/JSON data renderer with template expansion'
|
|
12
|
+
spec.description = 'A generic Ruby library that expands QueryStream notation in text content ' \
|
|
13
|
+
'by combining YAML/JSON data files with template files.'
|
|
14
|
+
spec.homepage = 'https://github.com/Atelier-Mirai/query-stream'
|
|
15
|
+
spec.license = 'MIT'
|
|
16
|
+
|
|
17
|
+
spec.metadata['source_code_uri'] = 'https://github.com/Atelier-Mirai/query-stream'
|
|
18
|
+
spec.metadata['changelog_uri'] = 'https://github.com/Atelier-Mirai/query-stream/blob/master/CHANGELOG.md'
|
|
19
|
+
|
|
20
|
+
spec.required_ruby_version = '>= 4.0'
|
|
21
|
+
|
|
22
|
+
spec.files = Dir.glob('{lib,bin}/**/*') + %w[README.md LICENSE Gemfile query-stream.gemspec]
|
|
23
|
+
spec.bindir = 'bin'
|
|
24
|
+
spec.executables = ['query-stream']
|
|
25
|
+
spec.require_paths = ['lib']
|
|
26
|
+
|
|
27
|
+
# Runtime dependencies
|
|
28
|
+
spec.add_dependency 'logger'
|
|
29
|
+
spec.add_dependency 'samovar', '~> 2.1'
|
|
30
|
+
|
|
31
|
+
# Development dependencies
|
|
32
|
+
spec.add_development_dependency 'bundler'
|
|
33
|
+
spec.add_development_dependency 'rake', '~> 13.2'
|
|
34
|
+
spec.add_development_dependency 'minitest', '~> 5.22'
|
|
35
|
+
spec.metadata['rubygems_mfa_required'] = 'false'
|
|
36
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: query-stream
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Atelier Mirai
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: logger
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: samovar
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: bundler
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rake
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '13.2'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '13.2'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: minitest
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '5.22'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '5.22'
|
|
82
|
+
description: A generic Ruby library that expands QueryStream notation in text content
|
|
83
|
+
by combining YAML/JSON data files with template files.
|
|
84
|
+
email:
|
|
85
|
+
- contact@atelier-mirai.net
|
|
86
|
+
executables:
|
|
87
|
+
- query-stream
|
|
88
|
+
extensions: []
|
|
89
|
+
extra_rdoc_files: []
|
|
90
|
+
files:
|
|
91
|
+
- Gemfile
|
|
92
|
+
- LICENSE
|
|
93
|
+
- README.md
|
|
94
|
+
- bin/query-stream
|
|
95
|
+
- lib/query_stream.rb
|
|
96
|
+
- lib/query_stream/command.rb
|
|
97
|
+
- lib/query_stream/configuration.rb
|
|
98
|
+
- lib/query_stream/data_resolver.rb
|
|
99
|
+
- lib/query_stream/errors.rb
|
|
100
|
+
- lib/query_stream/filter_engine.rb
|
|
101
|
+
- lib/query_stream/query_stream_parser.rb
|
|
102
|
+
- lib/query_stream/singularize.rb
|
|
103
|
+
- lib/query_stream/template_compiler.rb
|
|
104
|
+
- lib/query_stream/version.rb
|
|
105
|
+
- query-stream.gemspec
|
|
106
|
+
homepage: https://github.com/Atelier-Mirai/query-stream
|
|
107
|
+
licenses:
|
|
108
|
+
- MIT
|
|
109
|
+
metadata:
|
|
110
|
+
source_code_uri: https://github.com/Atelier-Mirai/query-stream
|
|
111
|
+
changelog_uri: https://github.com/Atelier-Mirai/query-stream/blob/master/CHANGELOG.md
|
|
112
|
+
rubygems_mfa_required: 'false'
|
|
113
|
+
rdoc_options: []
|
|
114
|
+
require_paths:
|
|
115
|
+
- lib
|
|
116
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
117
|
+
requirements:
|
|
118
|
+
- - ">="
|
|
119
|
+
- !ruby/object:Gem::Version
|
|
120
|
+
version: '4.0'
|
|
121
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
122
|
+
requirements:
|
|
123
|
+
- - ">="
|
|
124
|
+
- !ruby/object:Gem::Version
|
|
125
|
+
version: '0'
|
|
126
|
+
requirements: []
|
|
127
|
+
rubygems_version: 4.0.6
|
|
128
|
+
specification_version: 4
|
|
129
|
+
summary: QueryStream - YAML/JSON data renderer with template expansion
|
|
130
|
+
test_files: []
|