uniprop 0.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,282 @@
1
+ module UniProp
2
+ class MetaDataValidator
3
+ attr_reader :metadata
4
+
5
+ # @param [MetaData] metadata
6
+ def initialize(metadata)
7
+ @metadata = metadata
8
+ end
9
+
10
+ # メタデータが記述されているバージョンすべてで検証を実行
11
+ def validate
12
+ metadata.prop_data.versions.each do |version|
13
+ if version.has_version_metadata?
14
+ version.version_metadata.version_metadata_validator.run_all_validations
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ class VersionMetaDataValidator
21
+ attr_reader :version_metadata, :version
22
+
23
+ def initialize(version_metadata)
24
+ @version_metadata = version_metadata
25
+ @version = @version_metadata.version
26
+
27
+ # validate_files_shortage, validate_files_excessでは、実際のキャッシュとメタデータを照らし合わせて検証を行う必要があるため、@versionはEfficientVersionではなくVersionでなければならない。
28
+ if @version.class != Version
29
+ raise TypeError, "The argument must be a VersionMetaData object associated with Version object (not EfficientVersion)"
30
+ end
31
+ end
32
+
33
+ # 全ての検証を実行
34
+ def run_all_validations
35
+ puts "== validation results for version #{version.version_name} =="
36
+ validate_files_shortage(reconfirm: true)
37
+ validate_files_excess(reconfirm: false)
38
+ validate_properties_shortage
39
+ validate_properties_excess
40
+ validate_row_perfection
41
+ validate_column_perfection
42
+ validate_type
43
+
44
+ if version.has_unihan?
45
+ validate_unihan_properties_shortage
46
+ validate_unihan_properties_excess
47
+ end
48
+ end
49
+
50
+ # メタデータの記述が不足しているファイルを取得
51
+ # @return [Set<PropFile>]
52
+ def metadata_insufficient_files
53
+ return @metadata_insufficient_files if @metadata_insufficient_files
54
+
55
+ @metadata_insufficient_files = version.files.reject{_1.is_meta_file?} - version_metadata.actual_propfiles
56
+
57
+ @metadata_insufficient_files
58
+ end
59
+
60
+ # メタデータ内で記述が不足しているファイルを標準出力に出力
61
+ def validate_files_shortage(reconfirm: false)
62
+ version.files(reconfirm: reconfirm, reload: reconfirm) if reconfirm
63
+
64
+ if metadata_insufficient_files.empty?
65
+ puts "All files are described in the metadata"
66
+ else
67
+ puts "Files that are not described in the metadata"
68
+ metadata_insufficient_files.each { puts "・#{_1.basename_prefix}" }
69
+ end
70
+ end
71
+
72
+ # メタデータ内に余分に記述されたファイル(実際には存在しないにも関わらず、メタデータに記述されたファイル)を標準出力に出力
73
+ def validate_files_excess(reconfirm: false)
74
+ version.files(reconfirm: reconfirm, reload: reconfirm) if reconfirm
75
+
76
+ nonexist_files = version_metadata.propfile_names.filter { !version.has_file?(_1) }
77
+
78
+ if nonexist_files.empty?
79
+ puts "There are no excessive files in the metadata."
80
+ else
81
+ puts "Files that are described in the metadata even though it does not actually exist"
82
+ nonexist_files.each { puts "・#{_1}" }
83
+ end
84
+ end
85
+
86
+ # メタデータ内で記述が不足しているプロパティを標準出力に出力
87
+ def validate_properties_shortage
88
+ metadata_insufficient_properties = version.properties - version_metadata.actual_properties
89
+
90
+ if metadata_insufficient_properties.empty?
91
+ puts "All properties are described in the metadata"
92
+ else
93
+ puts "Properties that not described in the metadata"
94
+ metadata_insufficient_properties.each { puts "・#{_1.longest_alias}" }
95
+ end
96
+ end
97
+
98
+ # メタデータ内に余分に記述されたプロパティ(実際には存在しないにも関わらず、メタデータに記述されたプロパティ)を標準出力に出力
99
+ def validate_properties_excess
100
+ nonexist_props = version_metadata.property_names
101
+ .reject { _1=="" || _1=="codepoint" }
102
+ .reject { version.has_property?(_1) }
103
+
104
+ if nonexist_props.empty?
105
+ puts "There are no excessive properties in the metadata."
106
+ else
107
+ puts "Properties that's described in the metadata even though it does not actually exist"
108
+ nonexist_props.each { puts "・#{_1}" }
109
+ end
110
+ end
111
+
112
+ # information_containing_rangesのうち、block_rangesに含まれていない範囲を返す
113
+ # @param [Array<Range<Integer>>] block_ranges
114
+ # @param [Array<Range<Integer>>] information_containing_ranges
115
+ # @return [Array<Range<Integer>>]
116
+ def metadata_insufficient_ranges(block_ranges, information_containing_ranges)
117
+ result = information_containing_ranges
118
+ block_ranges.each do |block_range|
119
+ pre_result = result
120
+ result = []
121
+ pre_result.each { result.concat(UniPropUtils::RangeProcessor.cut_internal(_1, block_range.begin, block_range.end)) }
122
+ end
123
+ result
124
+ end
125
+
126
+ # 各PropFileの、メタデータに記述されていない行の範囲を取得
127
+ # @return [Hash<PropFile,Array<Range>]
128
+ def propfile_to_metadata_insufficient_ranges
129
+ return @propfile_to_metadata_insufficient_ranges if @propfile_to_metadata_insufficient_ranges
130
+
131
+ @propfile_to_metadata_insufficient_ranges = Hash.new { |hash,key| hash[key]=[] }
132
+
133
+ version_metadata.propfile_metadatas.each do |propfile_metadata|
134
+ propfile = propfile_metadata.propfile
135
+
136
+ @propfile_to_metadata_insufficient_ranges[propfile] = metadata_insufficient_ranges(propfile_metadata.property_written_ranges, propfile.information_containing_ranges)
137
+ end
138
+
139
+ @propfile_to_metadata_insufficient_ranges
140
+ end
141
+
142
+ # メタデータに記述されていない行の範囲が存在するファイルを標準出力に出力(Unihan, メタデータが記述されていないファイルを除く)
143
+ def validate_row_perfection
144
+ if propfile_to_metadata_insufficient_ranges.all? { _2.empty? }
145
+ puts "There are no files for which a file name is described in the metadata but for which this information is missing."
146
+ else
147
+ puts "Ranges where metadata is missing in propfiles"
148
+ propfile_to_metadata_insufficient_ranges.each do |propfile, insufficient_ranges|
149
+ if !insufficient_ranges.empty?
150
+ puts "・#{propfile.basename_prefix}"
151
+
152
+ insufficient_ranges.each do |range|
153
+ if range.size==1
154
+ puts "\t#{range.begin}"
155
+ else
156
+ puts "\t#{range.begin} to #{range.end}"
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+
164
+ # 実際の列数とメタデータに記述されている列数に違いがあるファイルを標準出力に出力
165
+ def validate_column_perfection
166
+ error_logs = []
167
+ version_metadata.propfile_metadatas.each do |propfile_metadata|
168
+ propfile_metadata.blocks.each_with_index do |block, block_no|
169
+ metadata_col_size = block.content.size
170
+ actual_col_size = propfile_metadata.propfile.max_column_size(block.range)
171
+
172
+ if metadata_col_size != actual_col_size
173
+ error_logs << "#{propfile_metadata.propfile.basename_prefix} (in block #{block_no})\n\tmetadata column size: #{metadata_col_size}\n\tactual column size: #{actual_col_size}"
174
+ end
175
+ end
176
+ end
177
+
178
+ if error_logs.empty?
179
+ puts "There are no difference in column size between the metadata and the actual description in all files"
180
+ else
181
+ puts "Files that the number of columns differs between the metadata and the actual description"
182
+ error_logs.each { puts _1 }
183
+ end
184
+ end
185
+
186
+ # validate_typeで出力するための理想の型の名称を取得
187
+ # @param [Property] prop
188
+ # @return [String]
189
+ def expected_type(prop)
190
+ type = prop.property_value_type
191
+
192
+ case type
193
+ when :string, :numeric
194
+ return type
195
+ when :binary, :catalog, :enumerated
196
+ return "#{type} (#{prop.longest_alias})"
197
+ when :miscellaneous
198
+ return prop.miscellaneous_format
199
+ end
200
+ end
201
+
202
+ # メタデータと実際のファイルで、記述されているべき値の型に違いがある箇所を出力
203
+ MismatchPosition = Struct.new(:propfile, :column, :expected_type, :row_ranges)
204
+ def validate_type
205
+ # 違いがある箇所を検出
206
+ mismatches = []
207
+
208
+ version_metadata.propfile_metadatas.each do |propfile_metadata|
209
+ propfile = propfile_metadata.propfile
210
+
211
+ propfile_metadata.blocks.each do |block|
212
+ block.content.each_with_index do |prop, col|
213
+ # 列の値がArrayを使用して記述されている場合、検証をスキップする
214
+ next if !prop || prop.class==Array
215
+
216
+ mismatch_row_ranges = UniPropUtils::RangeProcessor.sub(
217
+ block.range,
218
+ propfile.verbose_property_value_type_match_ranges(col, prop)
219
+ )
220
+
221
+ if !mismatch_row_ranges.empty?
222
+ mismatches << MismatchPosition.new(propfile, col, expected_type(prop), mismatch_row_ranges)
223
+ end
224
+ end
225
+ end
226
+ end
227
+
228
+ # 検出結果を出力
229
+ if mismatches.empty?
230
+ puts "In all blocks in the metadata, the type of the property described in the block matches the type of the values in the actual file."
231
+ else
232
+ mismatches.each do |mismatch|
233
+ puts "According to the metadata, column #{mismatch.column} in #{mismatch.propfile.basename_prefix} should be #{mismatch.expected_type} value, but the following line is wrong."
234
+
235
+ mismatch.row_ranges.each do |range|
236
+ if range.size==1
237
+ puts "\t#{range.begin}"
238
+ else
239
+ puts "\t#{range.begin} to #{range.end}"
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end
245
+
246
+ # メタデータに記述が不足しているUnihanプロパティを出力
247
+ def validate_unihan_properties_shortage
248
+ metadata_insufficient_properties = []
249
+
250
+ version.unihanprop.unihan_properties.each do |prop|
251
+ if version_metadata.unihan_property_names.all? { !prop.has_alias?(_1) }
252
+ metadata_insufficient_properties << prop
253
+ end
254
+ end
255
+
256
+ if metadata_insufficient_properties.empty?
257
+ puts "All Unihan properties are described in the metadata"
258
+ else
259
+ puts "Unihan properties that's not described in the metadata"
260
+ metadata_insufficient_properties.each { puts "・#{_1.longest_alias}" }
261
+ end
262
+ end
263
+
264
+ # メタデータ内に余分に記述されたUnihanプロパティ(実際には存在しないにも関わらず、メタデータに記述されたUnihanプロパティ)を出力
265
+ def validate_unihan_properties_excess
266
+ nonexist_prop_names = []
267
+
268
+ version_metadata.unihan_property_names.each do |prop_name|
269
+ if version.unihanprop.unihan_properties.all? { !_1.has_alias?(prop_name) }
270
+ nonexist_prop_names << prop_name
271
+ end
272
+ end
273
+
274
+ if nonexist_prop_names.empty?
275
+ puts "There are no excessive Unihan properties in the metadata."
276
+ else
277
+ puts "Unihan properties that's described in the metadata even though it does not actually exist"
278
+ nonexist_prop_names.each { puts "・#{_1}" }
279
+ end
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,293 @@
1
+ module UniProp
2
+ class PropData
3
+ attr_reader :excluded_extensions, :excluded_directories, :excluded_files, :metadata_path
4
+ attr_accessor :cache_path, :settings
5
+
6
+ # @note VersionMetaDataRecreator#output_metadata_revising_hintsなど、2つ以上のメタデータを使用して処理を行う場合がある。そのような場合に対応するため、PropDataは1つのsettings.rbと1つのメタデータを使用し、Unicodeのファイル全体を扱うためのクラスとした。
7
+ def initialize(settings_path, metadata_path=nil)
8
+ @settings = Settings.new(settings_path)
9
+ cache_path_str = ENV["UniPropCache"] || @settings.cache_path
10
+ @cache_path = Pathname.new(cache_path_str).cleanpath.expand_path
11
+ @metadata_path = metadata_path
12
+ end
13
+
14
+ # unicode.orgから存在するバージョンの一覧を取得し、それを元にVersionオブジェクトを作成する
15
+ # @note メタデータが紐づけられていればその情報を使用し、紐づけられていなければUnicode.orgから情報を取得
16
+ # @param [Boolean] update trueの場合バージョン一覧を再取得
17
+ # @return [Set<Version>]
18
+ def versions(update: false)
19
+ return @versions if @versions && !update
20
+
21
+ @versions = Set.new
22
+
23
+ if has_metadata?
24
+ metadata.version_names(update_metadata: true, confirm: update).each { @versions << Version.new(self, _1) }
25
+ else
26
+ UniPropUtils::DownloaderWrapper.get_version_names.each { @versions << Version.new(self, _1) }
27
+ end
28
+
29
+ @versions
30
+ end
31
+
32
+ # @return [EfficientVersion]
33
+ def find_efficient_version(version_name)
34
+ weight = Version.name_to_weight(version_name)
35
+ @weight_to_efficient_version ||= {}
36
+ return @weight_to_efficient_version[weight] if @weight_to_efficient_version[weight]
37
+
38
+ if metadata.version_names.map { Version.name_to_weight(_1) }.include?(weight)
39
+ @weight_to_efficient_version[weight] = EfficientVersion.new(self, version_name)
40
+ else
41
+ raise VersionNotMatchedError, "version #{version_name} is not exists"
42
+ end
43
+
44
+ @weight_to_efficient_version[weight]
45
+ end
46
+
47
+ # @param [String] version_name
48
+ # @param [Boolean] reconfirm trueの場合、version_nameに対応するバージョンが見つからない時にバージョン一覧を取得する
49
+ # @return [Version]
50
+ def find_version(version_name, reconfirm: true)
51
+ parsed_version_name = Version.parse(version_name)
52
+
53
+ versions.each do |version|
54
+ if version.major == parsed_version_name[:major] && version.minor == parsed_version_name[:minor] && version.tiny == parsed_version_name[:tiny]
55
+ return version
56
+ end
57
+ end
58
+
59
+ # 一致するバージョンが見つからなかった場合
60
+ if reconfirm
61
+ versions(update: true) # バージョン一覧を更新
62
+ find_version(version_name, reconfirm: false)
63
+ else
64
+ raise VersionNotMatchedError
65
+ end
66
+ end
67
+
68
+ # vesionsのうち、最も古いバージョンを取得
69
+ # @return [Version]
70
+ def oldest_version
71
+ versions.sort_by { _1.weight } .first
72
+ end
73
+
74
+ # versionsのうち、最も新しいバージョンを取得
75
+ # @return [Version]
76
+ def latest_version
77
+ versions.sort_by { _1.weight } .last
78
+ end
79
+
80
+ # @return [Metadata]
81
+ # @raise [MetaDataNotFoundError] PropDataのinitialize時にmetadata_pathを指定していない場合に発生
82
+ def metadata
83
+ return @metadata if @metadata
84
+
85
+ if metadata_path
86
+ @metadata = MetaData.new(self, metadata_path)
87
+ return @metadata
88
+ else
89
+ raise(MetaDataNotFoundError, "This PropData object doesn't have metadata path.")
90
+ end
91
+ end
92
+
93
+ # @return [EfficientMetadata]
94
+ # @raise [MetaDataNotFoundError] PropDataのinitialize時にmetadata_pathを指定していない場合に発生
95
+ def efficient_metadata
96
+ return @efficient_metadata if @efficient_metadata
97
+
98
+ if metadata_path
99
+ @efficient_metadata = EfficientMetaData.new(self, metadata_path)
100
+ return @efficient_metadata
101
+ else
102
+ raise(MetaDataNotFoundError, "This PropData object doesn't have metadata path.")
103
+ end
104
+ end
105
+
106
+ # PropDataがメタデータに紐づけられているかを判定
107
+ def has_metadata?
108
+ !!metadata rescue false
109
+ end
110
+
111
+ # version1とversion2の間で、ファイル名(basename_prefix)が同じPropFileのハッシュを作成
112
+ # @param [Version] version1 キーとされるバージョン
113
+ # @param [Version] version2 値とされるバージョン
114
+ # @return [Hash<PropFile, PropFile>]
115
+ def file_correspondence(version1, version2)
116
+ return @file_correspondences[version1][version2] if @file_correspondences&&@file_correspondences[version1]&&@file_correspondences[version1][version2]
117
+
118
+ propfile_to_same_name_propfile = {}
119
+ version1.files.each do |f1|
120
+ version2.files.each do |f2|
121
+ if Alias.canonical(f1.basename_prefix)==Alias.canonical(f2.basename_prefix)
122
+ propfile_to_same_name_propfile[f1] = f2
123
+ break # 1つのバージョン内にbasename_prefixが同じファイルは2つ以上無いので、1つ見つかった段階でbreakする
124
+ end
125
+ end
126
+ end
127
+
128
+ @file_correspondences ||= {}
129
+ @file_correspondences[version1] ||= {}
130
+ @file_correspondences[version1][version2] = propfile_to_same_name_propfile
131
+
132
+ @file_correspondences[version1][version2]
133
+ end
134
+
135
+ # metadata_pathのファイルをnew_metadataの内容に上書き
136
+ # @param [Array/Hash] new_metadata JSONとして解釈できるオブジェクト
137
+ # @raise [MetaDataNotFoundError] PropDataがメタデータに関連付けられていない場合に発生
138
+ def update_metadata(new_metadata)
139
+ if metadata_path && metadata_path.exist?
140
+ metadata_path.write(JSON.pretty_generate(new_metadata))
141
+ else
142
+ raise MetaDataNotFoundError
143
+ end
144
+ end
145
+
146
+ # @return [UnicodeManager]
147
+ def unicode_manager
148
+ @unicode_manager ||= UnicodeManager.new(self)
149
+ end
150
+
151
+ # @param [String] version_name
152
+ # @return [VersionManager]
153
+ def version_manager(version_name)
154
+ vm = version_managers.find { _1.version.weight==Version.name_to_weight(version_name) }
155
+
156
+ vm || raise(MetaDataNotFoundError, "MetaData for #{version_name} is not found.")
157
+ end
158
+
159
+ # メタデータに含まれる全バージョンのVersionManagerを作成
160
+ # @return [Array<VersionManager>]
161
+ def version_managers
162
+ return @version_managers if @version_managers
163
+
164
+ @version_managers = metadata.version_names
165
+ .filter { metadata.has_raw_version_metadata?(_1) }
166
+ .map { find_efficient_version(_1) }
167
+ .map { VersionManager.new(_1) }
168
+
169
+ @version_managers
170
+ end
171
+
172
+ # @param [Pathname] path
173
+ # @note pathが指定されていない場合、メタデータの先頭にproperty_をつけたファイルを使用
174
+ # @return [PropertyMetadata]
175
+ # @raise [MetaDataNotFoundError] pathが指定されているがそのpathが存在しない場合に発生
176
+ def property_metadata(path=nil)
177
+ return @property_metadata if @property_metadata
178
+
179
+ select_path = !!path
180
+ path ||= metadata_path.parent / ("property_"+metadata_path.basename.to_s)
181
+
182
+ if !path.exist?
183
+ if select_path
184
+ raise MetaDataNotFoundError, "#{path} is not found."
185
+ else
186
+ path.write([])
187
+ end
188
+ end
189
+
190
+ @property_metadata = PropertyMetaData.new(self, path)
191
+ end
192
+ end
193
+
194
+ # 設定ファイルから値を取得するためのクラス
195
+ class Settings
196
+ attr_reader :setting_names
197
+
198
+ # @param [Pathname] path 設定ファイルの絶対パス
199
+ def initialize(path)
200
+ require path
201
+ @setting_names = {
202
+ downloader_settings: DOWNLOADER_SETTINGS,
203
+ files_information: FILES_INFORMATION,
204
+ properties_information: PROPERTIES_INFORMATION
205
+ }
206
+ end
207
+
208
+ # @param [Symbol] setting_name 検索する設定項目名
209
+ # @param [String] version 検索するバージョン名
210
+ # @note version==nilの場合、デフォルト値を取得
211
+ # @param [keys] 検索を行う経路
212
+ def search(setting_name, version, *keys)
213
+ if version
214
+ result = setting_names.dig(setting_name, version.to_sym, *keys)
215
+ return result if result
216
+ end
217
+ setting_names.dig(setting_name, :default, *keys)
218
+ end
219
+
220
+ # @param [String] version
221
+ # @return [Pathname]
222
+ def cache_path(version=nil)
223
+ search(:downloader_settings, version, :cache_path)
224
+ end
225
+
226
+ # @param [String] version
227
+ # @return [Array<String>]
228
+ def excluded_extensions(version=nil)
229
+ search(:downloader_settings, version, :excluded_extensions)
230
+ end
231
+
232
+ # @param [String] version
233
+ # @return [Array<String>]
234
+ def excluded_directories(version=nil)
235
+ search(:downloader_settings, version, :excluded_directories)
236
+ end
237
+
238
+ # @param [String] version
239
+ # @return [Array<String>]
240
+ def excluded_files(version=nil)
241
+ search(:downloader_settings, version, :excluded_files)
242
+ end
243
+
244
+ # @param [String] version
245
+ # @return [Array<String>]
246
+ def included_files(version=nil)
247
+ search(:downloader_settings, version, :included_files)
248
+ end
249
+
250
+ # @param [String] version
251
+ # @return [Boolean]
252
+ def unicode_beta(version=nil)
253
+ search(:downloader_settings, version, :unicode_beta)
254
+ end
255
+
256
+ # @param [String] version
257
+ # @param [String] file
258
+ # @return [Hash<Symbol,String>]
259
+ def file_format(version, file)
260
+ # 優先順位
261
+ # (1) version, fileが両方一致する結果にマッチ
262
+ result = search(:files_information, version, :file_formats).find { _1[:file_name]==file }
263
+
264
+ # (2) defaultの中でfileが一致する結果にマッチ
265
+ result ||= search(:files_information, nil, :file_formats).find { _1[:file_name]==file }
266
+
267
+ # (3) default_file_formatを取得
268
+ result ||= search(:files_information, nil, :default_file_format)
269
+ result
270
+ end
271
+
272
+ # @param [String] version
273
+ # @return [Hash<Symbol,String>]
274
+ def unihan_file_format(version=nil)
275
+ search(:files_information, version, :unihan_file_format)
276
+ end
277
+
278
+ # @param [String] version
279
+ # @param [String] property
280
+ # @return [Hash<Symbol,Object>?]
281
+ def miscellaneous_format(version, property)
282
+ # 優先順位
283
+ # (1) version, propertyが両方マッチ
284
+ result = search(:properties_information, version, :miscellaneous_formats).find { _1[:property_name]==property }
285
+
286
+ # (2) defaultのうち、propertyがマッチ
287
+ result ||= search(:properties_information, nil, :miscellaneous_formats).find { _1[:property_name]==property }
288
+
289
+ # (2)までにマッチする値が存在しない場合、nilをそのまま返す
290
+ result
291
+ end
292
+ end
293
+ end