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