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.
@@ -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: []