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 +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 +25 -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 +225 -0
- data/query-stream.gemspec +33 -0
- metadata +128 -0
data/lib/query_stream.rb
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
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
|
+
# @param content [String] テキストコンテンツ
|
|
53
|
+
# @param source_filename [String, nil] エラー報告用のソースファイル名
|
|
54
|
+
# @param data_dir [String, nil] データディレクトリ(nil時はconfigを使用)
|
|
55
|
+
# @param templates_dir [String, nil] テンプレートディレクトリ(nil時はconfigを使用)
|
|
56
|
+
# @return [String] 展開後のテキストコンテンツ
|
|
57
|
+
def render(content, source_filename: nil, data_dir: nil, templates_dir: nil)
|
|
58
|
+
data_dir ||= configuration.data_dir
|
|
59
|
+
templates_dir ||= configuration.templates_dir
|
|
60
|
+
|
|
61
|
+
lines = content.lines
|
|
62
|
+
result = []
|
|
63
|
+
in_code_block = false
|
|
64
|
+
|
|
65
|
+
lines.each_with_index do |line, idx|
|
|
66
|
+
line_number = idx + 1
|
|
67
|
+
|
|
68
|
+
# コードブロック内はスキップ
|
|
69
|
+
if line.lstrip.start_with?('```')
|
|
70
|
+
in_code_block = !in_code_block
|
|
71
|
+
result << line
|
|
72
|
+
next
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if in_code_block
|
|
76
|
+
result << line
|
|
77
|
+
next
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# QueryStream 記法の検出
|
|
81
|
+
if line.match?(QUERY_STREAM_PATTERN)
|
|
82
|
+
expanded = render_query(
|
|
83
|
+
line.chomp, line_number:, source_filename:, data_dir:, templates_dir:
|
|
84
|
+
)
|
|
85
|
+
result << expanded << "\n"
|
|
86
|
+
else
|
|
87
|
+
result << line
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
result.join
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# 単一の QueryStream 記法を展開する
|
|
95
|
+
# @param query [String] QueryStream 記法の行(例: "= books | tags=ruby | :full")
|
|
96
|
+
# @param line_number [Integer, nil] 行番号(エラー報告用)
|
|
97
|
+
# @param source_filename [String, nil] ソースファイル名
|
|
98
|
+
# @param data_dir [String, nil] データディレクトリ
|
|
99
|
+
# @param templates_dir [String, nil] テンプレートディレクトリ
|
|
100
|
+
# @return [String] 展開後のテキスト
|
|
101
|
+
def render_query(query, line_number: nil, source_filename: nil, data_dir: nil, templates_dir: nil)
|
|
102
|
+
data_dir ||= configuration.data_dir
|
|
103
|
+
templates_dir ||= configuration.templates_dir
|
|
104
|
+
location = source_filename ? "#{source_filename}:#{line_number}" : "行#{line_number}"
|
|
105
|
+
|
|
106
|
+
# --- Phase: Parse ---
|
|
107
|
+
parsed = QueryStreamParser.parse(query)
|
|
108
|
+
|
|
109
|
+
# --- Phase: Load Data ---
|
|
110
|
+
data_file = DataResolver.resolve(parsed[:source], data_dir)
|
|
111
|
+
unless data_file
|
|
112
|
+
expected = File.join(data_dir, "#{parsed[:source]}.yml")
|
|
113
|
+
logger.error("データファイルが見つかりません(#{location})")
|
|
114
|
+
logger.error(" 記法: #{query}")
|
|
115
|
+
logger.error(" 期待: #{expected}")
|
|
116
|
+
raise DataNotFoundError, "データファイルが見つかりません: #{expected}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
records = DataResolver.load_records(data_file)
|
|
120
|
+
|
|
121
|
+
# --- Phase: Filter ---
|
|
122
|
+
records = FilterEngine.apply_filters(records, parsed[:filters])
|
|
123
|
+
|
|
124
|
+
# --- Phase: Sort ---
|
|
125
|
+
records = FilterEngine.apply_sort(records, parsed[:sort]) if parsed[:sort]
|
|
126
|
+
|
|
127
|
+
# --- Phase: Limit ---
|
|
128
|
+
records = records.first(parsed[:limit]) if parsed[:limit]
|
|
129
|
+
|
|
130
|
+
# --- Phase: Single record warning ---
|
|
131
|
+
if parsed[:single_lookup]
|
|
132
|
+
case records.size
|
|
133
|
+
when 0
|
|
134
|
+
logger.warn("一件検索で該当なし(#{location}): #{query}")
|
|
135
|
+
return ''
|
|
136
|
+
when 1
|
|
137
|
+
# 正常
|
|
138
|
+
else
|
|
139
|
+
logger.warn("一件検索で複数件ヒット(#{location}): #{query}")
|
|
140
|
+
logger.warn(" #{records.size}件見つかりました。条件を明示してください。")
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# --- Phase: Resolve Template ---
|
|
145
|
+
singular = Singularize.call(parsed[:source])
|
|
146
|
+
style = parsed[:style]
|
|
147
|
+
format = parsed[:format]
|
|
148
|
+
template_path = resolve_template_path(singular, style, format, templates_dir)
|
|
149
|
+
|
|
150
|
+
unless File.exist?(template_path)
|
|
151
|
+
hint = build_template_hint(singular, style, format, templates_dir)
|
|
152
|
+
logger.error("テンプレートファイルが見つかりません(#{location})")
|
|
153
|
+
logger.error(" 記法: #{query}")
|
|
154
|
+
logger.error(" 期待: #{template_path}")
|
|
155
|
+
logger.error(" ヒント: #{hint}") if hint
|
|
156
|
+
raise TemplateNotFoundError, "テンプレートファイルが見つかりません: #{template_path}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
template_content = File.read(template_path, encoding: 'utf-8')
|
|
160
|
+
|
|
161
|
+
# --- Phase: Render ---
|
|
162
|
+
TemplateCompiler.render(template_content, records, source_filename:, line_number:)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# テキスト内の QueryStream 記法を検出してリストを返す
|
|
166
|
+
# @param path_or_content [String] ファイルパスまたはテキストコンテンツ
|
|
167
|
+
# @return [Array<String>] 検出された QueryStream 記法のリスト
|
|
168
|
+
def scan(path_or_content)
|
|
169
|
+
content = File.exist?(path_or_content) ? File.read(path_or_content, encoding: 'utf-8') : path_or_content
|
|
170
|
+
lines = content.lines
|
|
171
|
+
queries = []
|
|
172
|
+
in_code_block = false
|
|
173
|
+
|
|
174
|
+
lines.each do |line|
|
|
175
|
+
if line.lstrip.start_with?('```')
|
|
176
|
+
in_code_block = !in_code_block
|
|
177
|
+
next
|
|
178
|
+
end
|
|
179
|
+
next if in_code_block
|
|
180
|
+
|
|
181
|
+
queries << line.chomp if line.match?(QUERY_STREAM_PATTERN)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
queries
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
# テンプレートファイルパスを解決する
|
|
190
|
+
# @param singular_name [String] 単数形のデータ名
|
|
191
|
+
# @param style [String, nil] スタイル名
|
|
192
|
+
# @param format [String, nil] 出力形式(拡張子)
|
|
193
|
+
# @param templates_dir [String] テンプレートディレクトリ
|
|
194
|
+
# @return [String] テンプレートファイルパス
|
|
195
|
+
def resolve_template_path(singular_name, style, format, templates_dir)
|
|
196
|
+
ext = format || configuration.default_format.to_s
|
|
197
|
+
ext = 'md' if ext == 'md' || ext.empty?
|
|
198
|
+
|
|
199
|
+
if style
|
|
200
|
+
# :table.html → _book.table.html
|
|
201
|
+
# :full → _book.full.md
|
|
202
|
+
if format
|
|
203
|
+
File.join(templates_dir, "_#{singular_name}.#{style}.#{format}")
|
|
204
|
+
else
|
|
205
|
+
File.join(templates_dir, "_#{singular_name}.#{style}.#{ext}")
|
|
206
|
+
end
|
|
207
|
+
else
|
|
208
|
+
File.join(templates_dir, "_#{singular_name}.#{ext}")
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# テンプレート不在時のヒントメッセージを生成する
|
|
213
|
+
def build_template_hint(singular_name, style, format, templates_dir)
|
|
214
|
+
default_ext = format || configuration.default_format.to_s
|
|
215
|
+
default_ext = 'md' if default_ext == 'md' || default_ext.empty?
|
|
216
|
+
default_path = File.join(templates_dir, "_#{singular_name}.#{default_ext}")
|
|
217
|
+
|
|
218
|
+
if style && File.exist?(default_path)
|
|
219
|
+
"#{default_path} は存在します。スタイル名を確認してください。"
|
|
220
|
+
else
|
|
221
|
+
nil
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
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.required_ruby_version = '>= 4.0'
|
|
18
|
+
|
|
19
|
+
spec.files = Dir.glob('{lib,bin}/**/*') + %w[README.md LICENSE Gemfile query-stream.gemspec]
|
|
20
|
+
spec.bindir = 'bin'
|
|
21
|
+
spec.executables = ['query-stream']
|
|
22
|
+
spec.require_paths = ['lib']
|
|
23
|
+
|
|
24
|
+
# Runtime dependencies
|
|
25
|
+
spec.add_dependency 'logger'
|
|
26
|
+
spec.add_dependency 'samovar', '~> 2.1'
|
|
27
|
+
|
|
28
|
+
# Development dependencies
|
|
29
|
+
spec.add_development_dependency 'bundler'
|
|
30
|
+
spec.add_development_dependency 'rake', '~> 13.2'
|
|
31
|
+
spec.add_development_dependency 'minitest', '~> 5.22'
|
|
32
|
+
spec.metadata['rubygems_mfa_required'] = 'false'
|
|
33
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: query-stream
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.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
|
+
rubygems_mfa_required: 'false'
|
|
111
|
+
rdoc_options: []
|
|
112
|
+
require_paths:
|
|
113
|
+
- lib
|
|
114
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
115
|
+
requirements:
|
|
116
|
+
- - ">="
|
|
117
|
+
- !ruby/object:Gem::Version
|
|
118
|
+
version: '4.0'
|
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0'
|
|
124
|
+
requirements: []
|
|
125
|
+
rubygems_version: 4.0.8
|
|
126
|
+
specification_version: 4
|
|
127
|
+
summary: QueryStream - YAML/JSON data renderer with template expansion
|
|
128
|
+
test_files: []
|