query-stream 1.2.2

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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QueryStream
4
+ VERSION = '1.2.2'
5
+ end
@@ -0,0 +1,244 @@
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
+ # @param on_warning [Proc, nil] 警告時コールバック。|warning| を受け取る。省略時は何もしない。
60
+ # @return [String] 展開後のテキストコンテンツ
61
+ def render(content, source_filename: nil, data_dir: nil, templates_dir: nil, on_error: nil, on_warning: nil)
62
+ data_dir ||= configuration.data_dir
63
+ templates_dir ||= configuration.templates_dir
64
+
65
+ lines = content.lines
66
+ result = []
67
+ in_code_block = false
68
+
69
+ lines.each_with_index do |line, idx|
70
+ line_number = idx + 1
71
+
72
+ # コードブロック内はスキップ
73
+ if line.lstrip.start_with?('```')
74
+ in_code_block = !in_code_block
75
+ result << line
76
+ next
77
+ end
78
+
79
+ if in_code_block
80
+ result << line
81
+ next
82
+ end
83
+
84
+ # QueryStream 記法の検出
85
+ if line.match?(QUERY_STREAM_PATTERN)
86
+ begin
87
+ expanded = render_query(
88
+ line.chomp, line_number:, source_filename:, data_dir:, templates_dir:, on_warning:
89
+ )
90
+ result << expanded << "\n"
91
+ rescue Error => e
92
+ # 1行の失敗で残りの展開を止めない。エラー処理は呼び出し元に委ねる。
93
+ on_error&.call(e)
94
+ result << line
95
+ end
96
+ else
97
+ result << line
98
+ end
99
+ end
100
+
101
+ result.join
102
+ end
103
+
104
+ # 単一の QueryStream 記法を展開する
105
+ # @param query [String] QueryStream 記法の行(例: "= books | tags=ruby | :full")
106
+ # @param line_number [Integer, nil] 行番号(エラー報告用)
107
+ # @param source_filename [String, nil] ソースファイル名
108
+ # @param data_dir [String, nil] データディレクトリ
109
+ # @param templates_dir [String, nil] テンプレートディレクトリ
110
+ # @return [String] 展開後のテキスト
111
+ def render_query(query, line_number: nil, source_filename: nil, data_dir: nil, templates_dir: nil, on_warning: nil)
112
+ data_dir ||= configuration.data_dir
113
+ templates_dir ||= configuration.templates_dir
114
+ location = source_filename ? "#{source_filename}:#{line_number}" : "行#{line_number}"
115
+
116
+ # --- Phase: Parse ---
117
+ parsed = QueryStreamParser.parse(query)
118
+
119
+ # --- Phase: Load Data ---
120
+ # gem 内でログ出力せず、構造化された属性を持つ例外を raise する。
121
+ # メッセージの構成・ログ出力・i18n は呼び出し元の責務とする。
122
+ data_file = DataResolver.resolve(parsed[:source], data_dir)
123
+ unless data_file
124
+ expected = File.join(data_dir, "#{parsed[:source]}.yml")
125
+ raise DataNotFoundError.new(
126
+ expected_path: expected,
127
+ query: query,
128
+ location: location
129
+ )
130
+ end
131
+
132
+ records = DataResolver.load_records(data_file)
133
+
134
+ # --- Phase: Filter ---
135
+ records = FilterEngine.apply_filters(records, parsed[:filters])
136
+
137
+ # --- Phase: Sort ---
138
+ records = FilterEngine.apply_sort(records, parsed[:sort]) if parsed[:sort]
139
+
140
+ # --- Phase: Limit ---
141
+ records = records.first(parsed[:limit]) if parsed[:limit]
142
+
143
+ # --- Phase: Single record warning ---
144
+ if parsed[:single_lookup]
145
+ case records.size
146
+ when 0
147
+ # gem 内でログ出力せず、構造化された属性を持つ警告を呼び出し元に委ねる。
148
+ on_warning&.call(NoResultWarning.new(query: query, location: location))
149
+ return ''
150
+ when 1
151
+ # 正常
152
+ else
153
+ # gem 内でログ出力せず、構造化された属性を持つ警告を呼び出し元に委ねる。
154
+ on_warning&.call(
155
+ AmbiguousQueryWarning.new(query: query, location: location, count: records.size)
156
+ )
157
+ end
158
+ end
159
+
160
+ # --- Phase: Resolve Template ---
161
+ singular = Singularize.call(parsed[:source])
162
+ style = parsed[:style]
163
+ format = parsed[:format]
164
+ template_path = resolve_template_path(singular, style, format, templates_dir)
165
+
166
+ unless File.exist?(template_path)
167
+ hint = build_template_hint(singular, style, format, templates_dir)
168
+ # gem 内でログ出力せず、構造化された属性を持つ例外を raise する。
169
+ # メッセージの構成・ログ出力・i18n は呼び出し元の責務とする。
170
+ raise TemplateNotFoundError.new(
171
+ template_path: template_path,
172
+ query: query,
173
+ location: location,
174
+ hint: hint
175
+ )
176
+ end
177
+
178
+ template_content = File.read(template_path, encoding: 'utf-8')
179
+
180
+ # --- Phase: Render ---
181
+ TemplateCompiler.render(template_content, records, source_filename:, line_number:)
182
+ end
183
+
184
+ # テキスト内の QueryStream 記法を検出してリストを返す
185
+ # @param path_or_content [String] ファイルパスまたはテキストコンテンツ
186
+ # @return [Array<String>] 検出された QueryStream 記法のリスト
187
+ def scan(path_or_content)
188
+ content = File.exist?(path_or_content) ? File.read(path_or_content, encoding: 'utf-8') : path_or_content
189
+ lines = content.lines
190
+ queries = []
191
+ in_code_block = false
192
+
193
+ lines.each do |line|
194
+ if line.lstrip.start_with?('```')
195
+ in_code_block = !in_code_block
196
+ next
197
+ end
198
+ next if in_code_block
199
+
200
+ queries << line.chomp if line.match?(QUERY_STREAM_PATTERN)
201
+ end
202
+
203
+ queries
204
+ end
205
+
206
+ private
207
+
208
+ # テンプレートファイルパスを解決する
209
+ # @param singular_name [String] 単数形のデータ名
210
+ # @param style [String, nil] スタイル名
211
+ # @param format [String, nil] 出力形式(拡張子)
212
+ # @param templates_dir [String] テンプレートディレクトリ
213
+ # @return [String] テンプレートファイルパス
214
+ def resolve_template_path(singular_name, style, format, templates_dir)
215
+ ext = format || configuration.default_format.to_s
216
+ ext = 'md' if ext == 'md' || ext.empty?
217
+
218
+ if style
219
+ # :table.html → _book.table.html
220
+ # :full → _book.full.md
221
+ if format
222
+ File.join(templates_dir, "_#{singular_name}.#{style}.#{format}")
223
+ else
224
+ File.join(templates_dir, "_#{singular_name}.#{style}.#{ext}")
225
+ end
226
+ else
227
+ File.join(templates_dir, "_#{singular_name}.#{ext}")
228
+ end
229
+ end
230
+
231
+ # テンプレート不在時のヒントメッセージを生成する
232
+ def build_template_hint(singular_name, style, format, templates_dir)
233
+ default_ext = format || configuration.default_format.to_s
234
+ default_ext = 'md' if default_ext == 'md' || default_ext.empty?
235
+ default_path = File.join(templates_dir, "_#{singular_name}.#{default_ext}")
236
+
237
+ if style && File.exist?(default_path)
238
+ "#{default_path} は存在します。スタイル名を確認してください。"
239
+ else
240
+ nil
241
+ end
242
+ end
243
+ end
244
+ 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.2.2
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.10
128
+ specification_version: 4
129
+ summary: QueryStream - YAML/JSON data renderer with template expansion
130
+ test_files: []