coradoc 1.1.8 → 2.0.12

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.
Files changed (225) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -1
  3. data/Rakefile +3 -12
  4. data/exe/coradoc +21 -2
  5. data/lib/coradoc/cli.rb +185 -91
  6. data/lib/coradoc/configurable.rb +527 -0
  7. data/lib/coradoc/coradoc.rb +463 -0
  8. data/lib/coradoc/core_model/annotation_block.rb +57 -0
  9. data/lib/coradoc/core_model/base.rb +172 -0
  10. data/lib/coradoc/core_model/bibliography.rb +41 -0
  11. data/lib/coradoc/core_model/bibliography_entry.rb +48 -0
  12. data/lib/coradoc/core_model/block.rb +63 -0
  13. data/lib/coradoc/core_model/children_content.rb +53 -0
  14. data/lib/coradoc/core_model/comment_block.rb +10 -0
  15. data/lib/coradoc/core_model/definition_item.rb +46 -0
  16. data/lib/coradoc/core_model/definition_list.rb +28 -0
  17. data/lib/coradoc/core_model/element_attribute.rb +26 -0
  18. data/lib/coradoc/core_model/example_block.rb +10 -0
  19. data/lib/coradoc/core_model/footnote.rb +92 -0
  20. data/lib/coradoc/core_model/horizontal_rule_block.rb +10 -0
  21. data/lib/coradoc/core_model/id_generator.rb +16 -0
  22. data/lib/coradoc/core_model/image.rb +66 -0
  23. data/lib/coradoc/core_model/inline_element.rb +140 -0
  24. data/lib/coradoc/core_model/list_block.rb +135 -0
  25. data/lib/coradoc/core_model/list_item.rb +142 -0
  26. data/lib/coradoc/core_model/listing_block.rb +13 -0
  27. data/lib/coradoc/core_model/literal_block.rb +10 -0
  28. data/lib/coradoc/core_model/metadata.rb +79 -0
  29. data/lib/coradoc/core_model/open_block.rb +10 -0
  30. data/lib/coradoc/core_model/paragraph_block.rb +10 -0
  31. data/lib/coradoc/core_model/pass_block.rb +10 -0
  32. data/lib/coradoc/core_model/quote_block.rb +12 -0
  33. data/lib/coradoc/core_model/reviewer_block.rb +10 -0
  34. data/lib/coradoc/core_model/sidebar_block.rb +10 -0
  35. data/lib/coradoc/core_model/source_block.rb +10 -0
  36. data/lib/coradoc/core_model/structural_element.rb +94 -0
  37. data/lib/coradoc/core_model/table.rb +148 -0
  38. data/lib/coradoc/core_model/term.rb +53 -0
  39. data/lib/coradoc/core_model/text_content.rb +22 -0
  40. data/lib/coradoc/core_model/toc.rb +105 -0
  41. data/lib/coradoc/core_model/toc_generator.rb +151 -0
  42. data/lib/coradoc/core_model/verse_block.rb +12 -0
  43. data/lib/coradoc/core_model.rb +77 -0
  44. data/lib/coradoc/document_builder.rb +184 -0
  45. data/lib/coradoc/document_manipulator.rb +203 -0
  46. data/lib/coradoc/errors.rb +312 -0
  47. data/lib/coradoc/format_module.rb +49 -0
  48. data/lib/coradoc/hooks.rb +176 -0
  49. data/lib/coradoc/input.rb +17 -7
  50. data/lib/coradoc/logger.rb +54 -0
  51. data/lib/coradoc/output.rb +17 -6
  52. data/lib/coradoc/performance_regression.rb +109 -0
  53. data/lib/coradoc/processor_registry.rb +50 -0
  54. data/lib/coradoc/query.rb +455 -0
  55. data/lib/coradoc/registry.rb +156 -0
  56. data/lib/coradoc/serializer/registry.rb +150 -0
  57. data/lib/coradoc/transform.rb +11 -0
  58. data/lib/coradoc/validation.rb +646 -0
  59. data/lib/coradoc/version.rb +1 -1
  60. data/lib/coradoc/visitor.rb +283 -0
  61. data/lib/coradoc.rb +40 -19
  62. metadata +67 -277
  63. data/.editorconfig +0 -15
  64. data/.envrc +0 -1
  65. data/.irbrc +0 -1
  66. data/.pryrc.sample +0 -1
  67. data/.rubocop.yml +0 -14
  68. data/.rubocop_todo.yml +0 -179
  69. data/CHANGELOG.md +0 -9
  70. data/CODE_OF_CONDUCT.md +0 -84
  71. data/Dockerfile +0 -19
  72. data/Gemfile +0 -16
  73. data/LICENSE.txt +0 -21
  74. data/Makefile +0 -35
  75. data/README.Docker.adoc +0 -57
  76. data/README.adoc +0 -119
  77. data/coradoc.gemspec +0 -40
  78. data/docker-compose.yml +0 -14
  79. data/exe/reverse_adoc +0 -81
  80. data/exe/w2a +0 -60
  81. data/flake.lock +0 -114
  82. data/flake.nix +0 -135
  83. data/lib/coradoc/converter.rb +0 -144
  84. data/lib/coradoc/document.rb +0 -77
  85. data/lib/coradoc/element/admonition.rb +0 -18
  86. data/lib/coradoc/element/attribute.rb +0 -36
  87. data/lib/coradoc/element/attribute_list.rb +0 -138
  88. data/lib/coradoc/element/audio.rb +0 -33
  89. data/lib/coradoc/element/author.rb +0 -24
  90. data/lib/coradoc/element/base.rb +0 -92
  91. data/lib/coradoc/element/bibliography.rb +0 -24
  92. data/lib/coradoc/element/bibliography_entry.rb +0 -24
  93. data/lib/coradoc/element/block/core.rb +0 -76
  94. data/lib/coradoc/element/block/example.rb +0 -23
  95. data/lib/coradoc/element/block/listing.rb +0 -21
  96. data/lib/coradoc/element/block/literal.rb +0 -21
  97. data/lib/coradoc/element/block/open.rb +0 -22
  98. data/lib/coradoc/element/block/pass.rb +0 -21
  99. data/lib/coradoc/element/block/quote.rb +0 -19
  100. data/lib/coradoc/element/block/reviewer_comment.rb +0 -19
  101. data/lib/coradoc/element/block/side.rb +0 -19
  102. data/lib/coradoc/element/block/sourcecode.rb +0 -21
  103. data/lib/coradoc/element/block.rb +0 -17
  104. data/lib/coradoc/element/break.rb +0 -11
  105. data/lib/coradoc/element/comment_block.rb +0 -22
  106. data/lib/coradoc/element/comment_line.rb +0 -18
  107. data/lib/coradoc/element/document_attributes.rb +0 -33
  108. data/lib/coradoc/element/header.rb +0 -22
  109. data/lib/coradoc/element/image/block_image.rb +0 -32
  110. data/lib/coradoc/element/image/core.rb +0 -58
  111. data/lib/coradoc/element/image/inline_image.rb +0 -12
  112. data/lib/coradoc/element/image.rb +0 -10
  113. data/lib/coradoc/element/include.rb +0 -18
  114. data/lib/coradoc/element/inline/anchor.rb +0 -19
  115. data/lib/coradoc/element/inline/attribute_reference.rb +0 -19
  116. data/lib/coradoc/element/inline/bold.rb +0 -25
  117. data/lib/coradoc/element/inline/cross_reference.rb +0 -46
  118. data/lib/coradoc/element/inline/footnote.rb +0 -24
  119. data/lib/coradoc/element/inline/hard_line_break.rb +0 -11
  120. data/lib/coradoc/element/inline/highlight.rb +0 -25
  121. data/lib/coradoc/element/inline/italic.rb +0 -25
  122. data/lib/coradoc/element/inline/link.rb +0 -42
  123. data/lib/coradoc/element/inline/monospace.rb +0 -25
  124. data/lib/coradoc/element/inline/quotation.rb +0 -20
  125. data/lib/coradoc/element/inline/small.rb +0 -19
  126. data/lib/coradoc/element/inline/span.rb +0 -37
  127. data/lib/coradoc/element/inline/subscript.rb +0 -20
  128. data/lib/coradoc/element/inline/superscript.rb +0 -20
  129. data/lib/coradoc/element/inline/underline.rb +0 -19
  130. data/lib/coradoc/element/inline.rb +0 -23
  131. data/lib/coradoc/element/list/core.rb +0 -51
  132. data/lib/coradoc/element/list/definition.rb +0 -29
  133. data/lib/coradoc/element/list/ordered.rb +0 -17
  134. data/lib/coradoc/element/list/unordered.rb +0 -17
  135. data/lib/coradoc/element/list.rb +0 -13
  136. data/lib/coradoc/element/list_item.rb +0 -98
  137. data/lib/coradoc/element/list_item_definition.rb +0 -32
  138. data/lib/coradoc/element/paragraph.rb +0 -37
  139. data/lib/coradoc/element/revision.rb +0 -27
  140. data/lib/coradoc/element/section.rb +0 -62
  141. data/lib/coradoc/element/table.rb +0 -91
  142. data/lib/coradoc/element/tag.rb +0 -19
  143. data/lib/coradoc/element/term.rb +0 -22
  144. data/lib/coradoc/element/text_element.rb +0 -92
  145. data/lib/coradoc/element/title.rb +0 -62
  146. data/lib/coradoc/element/video.rb +0 -50
  147. data/lib/coradoc/generator.rb +0 -19
  148. data/lib/coradoc/input/adoc.rb +0 -30
  149. data/lib/coradoc/input/docx.rb +0 -64
  150. data/lib/coradoc/input/html/LICENSE.txt +0 -25
  151. data/lib/coradoc/input/html/README.adoc +0 -308
  152. data/lib/coradoc/input/html/cleaner.rb +0 -142
  153. data/lib/coradoc/input/html/config.rb +0 -77
  154. data/lib/coradoc/input/html/converters/a.rb +0 -52
  155. data/lib/coradoc/input/html/converters/aside.rb +0 -16
  156. data/lib/coradoc/input/html/converters/audio.rb +0 -29
  157. data/lib/coradoc/input/html/converters/base.rb +0 -108
  158. data/lib/coradoc/input/html/converters/blockquote.rb +0 -22
  159. data/lib/coradoc/input/html/converters/br.rb +0 -15
  160. data/lib/coradoc/input/html/converters/bypass.rb +0 -81
  161. data/lib/coradoc/input/html/converters/code.rb +0 -23
  162. data/lib/coradoc/input/html/converters/div.rb +0 -19
  163. data/lib/coradoc/input/html/converters/dl.rb +0 -62
  164. data/lib/coradoc/input/html/converters/drop.rb +0 -26
  165. data/lib/coradoc/input/html/converters/em.rb +0 -21
  166. data/lib/coradoc/input/html/converters/figure.rb +0 -25
  167. data/lib/coradoc/input/html/converters/h.rb +0 -42
  168. data/lib/coradoc/input/html/converters/head.rb +0 -23
  169. data/lib/coradoc/input/html/converters/hr.rb +0 -15
  170. data/lib/coradoc/input/html/converters/ignore.rb +0 -20
  171. data/lib/coradoc/input/html/converters/img.rb +0 -110
  172. data/lib/coradoc/input/html/converters/li.rb +0 -17
  173. data/lib/coradoc/input/html/converters/mark.rb +0 -19
  174. data/lib/coradoc/input/html/converters/markup.rb +0 -31
  175. data/lib/coradoc/input/html/converters/math.rb +0 -38
  176. data/lib/coradoc/input/html/converters/ol.rb +0 -65
  177. data/lib/coradoc/input/html/converters/p.rb +0 -23
  178. data/lib/coradoc/input/html/converters/pass_through.rb +0 -17
  179. data/lib/coradoc/input/html/converters/pre.rb +0 -55
  180. data/lib/coradoc/input/html/converters/q.rb +0 -16
  181. data/lib/coradoc/input/html/converters/strong.rb +0 -20
  182. data/lib/coradoc/input/html/converters/sub.rb +0 -22
  183. data/lib/coradoc/input/html/converters/sup.rb +0 -22
  184. data/lib/coradoc/input/html/converters/table.rb +0 -319
  185. data/lib/coradoc/input/html/converters/td.rb +0 -81
  186. data/lib/coradoc/input/html/converters/text.rb +0 -32
  187. data/lib/coradoc/input/html/converters/th.rb +0 -18
  188. data/lib/coradoc/input/html/converters/tr.rb +0 -22
  189. data/lib/coradoc/input/html/converters/video.rb +0 -29
  190. data/lib/coradoc/input/html/converters.rb +0 -59
  191. data/lib/coradoc/input/html/errors.rb +0 -14
  192. data/lib/coradoc/input/html/html_converter.rb +0 -168
  193. data/lib/coradoc/input/html/plugin.rb +0 -131
  194. data/lib/coradoc/input/html/plugins/plateau.rb +0 -213
  195. data/lib/coradoc/input/html/postprocessor.rb +0 -220
  196. data/lib/coradoc/input/html.rb +0 -61
  197. data/lib/coradoc/legacy_parser.rb +0 -200
  198. data/lib/coradoc/oscal.rb +0 -99
  199. data/lib/coradoc/output/adoc.rb +0 -19
  200. data/lib/coradoc/output/coradoc_tree_debug.rb +0 -21
  201. data/lib/coradoc/parser/asciidoc/admonition.rb +0 -24
  202. data/lib/coradoc/parser/asciidoc/attribute_list.rb +0 -89
  203. data/lib/coradoc/parser/asciidoc/base.rb +0 -87
  204. data/lib/coradoc/parser/asciidoc/bibliography.rb +0 -29
  205. data/lib/coradoc/parser/asciidoc/block.rb +0 -94
  206. data/lib/coradoc/parser/asciidoc/citation.rb +0 -30
  207. data/lib/coradoc/parser/asciidoc/content.rb +0 -64
  208. data/lib/coradoc/parser/asciidoc/document_attributes.rb +0 -25
  209. data/lib/coradoc/parser/asciidoc/header.rb +0 -29
  210. data/lib/coradoc/parser/asciidoc/inline.rb +0 -195
  211. data/lib/coradoc/parser/asciidoc/list.rb +0 -115
  212. data/lib/coradoc/parser/asciidoc/paragraph.rb +0 -54
  213. data/lib/coradoc/parser/asciidoc/section.rb +0 -61
  214. data/lib/coradoc/parser/asciidoc/table.rb +0 -32
  215. data/lib/coradoc/parser/asciidoc/term.rb +0 -41
  216. data/lib/coradoc/parser/asciidoc/text.rb +0 -158
  217. data/lib/coradoc/parser/base.rb +0 -40
  218. data/lib/coradoc/parser.rb +0 -11
  219. data/lib/coradoc/reverse_adoc.rb +0 -18
  220. data/lib/coradoc/transformer.rb +0 -476
  221. data/lib/coradoc/util.rb +0 -12
  222. data/lib/reverse_adoc.rb +0 -20
  223. data/utils/inspect_asciidoc.rb +0 -29
  224. data/utils/parser_analyzer.rb +0 -66
  225. data/utils/round_trip.rb +0 -53
@@ -0,0 +1,646 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ # Document validation framework for schema-based validation.
5
+ #
6
+ # This module provides a flexible validation framework for ensuring
7
+ # documents conform to expected structures and rules.
8
+ #
9
+ # @example Creating a validation schema
10
+ # schema = Coradoc::Validation::Schema.define do
11
+ # required :title, type: String, min_length: 1
12
+ # required :sections, type: Array, min_count: 1
13
+ # optional :author, type: String
14
+ #
15
+ # rule :check_references do |doc|
16
+ # refs = doc.query('xref')
17
+ # missing = refs.reject { |r| doc.resolve_reference(r) }
18
+ # missing.map { |r| "Unresolved reference: #{r.target}" }
19
+ # end
20
+ # end
21
+ #
22
+ # @example Validating a document
23
+ # result = schema.validate(document)
24
+ # if result.valid?
25
+ # puts "Document is valid"
26
+ # else
27
+ # result.errors.each { |e| puts e.message }
28
+ # end
29
+ #
30
+ module Validation
31
+ # A single validation error
32
+ class Error
33
+ attr_reader :path, :message, :code, :element
34
+
35
+ # Create a validation error
36
+ #
37
+ # @param message [String] Error message
38
+ # @param path [String, nil] Path to the error location
39
+ # @param code [Symbol, nil] Error code for programmatic handling
40
+ # @param element [Object, nil] The element that failed validation
41
+ def initialize(message, path: nil, code: nil, element: nil)
42
+ @message = message
43
+ @path = path
44
+ @code = code
45
+ @element = element
46
+ end
47
+
48
+ # Format error as string
49
+ #
50
+ # @return [String]
51
+ def to_s
52
+ if path
53
+ "#{path}: #{message}"
54
+ else
55
+ message
56
+ end
57
+ end
58
+
59
+ # Convert to hash
60
+ #
61
+ # @return [Hash]
62
+ def to_h
63
+ { message: message, path: path, code: code }
64
+ end
65
+ end
66
+
67
+ # Validation result containing errors
68
+ class Result
69
+ attr_reader :errors, :warnings
70
+
71
+ # Create a validation result
72
+ #
73
+ # @param errors [Array<Error>] Validation errors
74
+ # @param warnings [Array<Error>] Validation warnings
75
+ def initialize(errors: [], warnings: [])
76
+ @errors = Array(errors)
77
+ @warnings = Array(warnings)
78
+ end
79
+
80
+ # Check if validation passed
81
+ #
82
+ # @return [Boolean]
83
+ def valid?
84
+ @errors.empty?
85
+ end
86
+
87
+ # Check if there are any warnings
88
+ #
89
+ # @return [Boolean]
90
+ def warnings?
91
+ @warnings.any?
92
+ end
93
+
94
+ # Get error count
95
+ #
96
+ # @return [Integer]
97
+ def error_count
98
+ @errors.size
99
+ end
100
+
101
+ # Get warning count
102
+ #
103
+ # @return [Integer]
104
+ def warning_count
105
+ @warnings.size
106
+ end
107
+
108
+ # Add an error
109
+ #
110
+ # @param message [String] Error message
111
+ # @param path [String, nil] Error path
112
+ # @param code [Symbol, nil] Error code
113
+ # @param element [Object, nil] Failed element
114
+ # @return [Error] The added error
115
+ def add_error(message, path: nil, code: nil, element: nil)
116
+ error = Error.new(message, path: path, code: code, element: element)
117
+ @errors << error
118
+ error
119
+ end
120
+
121
+ # Add a warning
122
+ #
123
+ # @param message [String] Warning message
124
+ # @param path [String, nil] Warning path
125
+ # @param code [Symbol, nil] Warning code
126
+ # @param element [Object, nil] Related element
127
+ # @return [Error] The added warning
128
+ def add_warning(message, path: nil, code: nil, element: nil)
129
+ warning = Error.new(message, path: path, code: code, element: element)
130
+ @warnings << warning
131
+ warning
132
+ end
133
+
134
+ # Merge another result into this one
135
+ #
136
+ # @param other [Result] Another validation result
137
+ # @return [void]
138
+ def merge!(other)
139
+ @errors.concat(other.errors)
140
+ @warnings.concat(other.warnings)
141
+ end
142
+
143
+ # Format errors as a human-readable string
144
+ #
145
+ # @return [String]
146
+ def to_s
147
+ return 'Valid' if valid?
148
+
149
+ lines = ["#{error_count} validation error(s):"]
150
+ @errors.each { |err| lines << " - #{err.path}: #{err.message}" }
151
+ lines.join("\n")
152
+ end
153
+
154
+ # Get errors for a specific path
155
+ #
156
+ # @param path [String] The path to filter by
157
+ # @return [Array<Error>]
158
+ def errors_at(path)
159
+ @errors.select { |e| e.path == path }
160
+ end
161
+
162
+ # Convert to hash
163
+ #
164
+ # @return [Hash]
165
+ def to_h
166
+ {
167
+ valid: valid?,
168
+ error_count: error_count,
169
+ warning_count: warning_count,
170
+ errors: @errors.map(&:to_h),
171
+ warnings: @warnings.map(&:to_h)
172
+ }
173
+ end
174
+ end
175
+
176
+ # Base class for validation rules
177
+ class Rule
178
+ attr_reader :name, :options
179
+
180
+ def initialize(name, **options)
181
+ @name = name
182
+ @options = options
183
+ end
184
+
185
+ def validate(element, context = {})
186
+ raise NotImplementedError, 'Subclasses must implement #validate'
187
+ end
188
+
189
+ private
190
+
191
+ def field_value(element, field)
192
+ if element.is_a?(CoreModel::Base)
193
+ element.public_send(field) if element.class.attributes.key?(field)
194
+ else
195
+ element.public_send(field)
196
+ end
197
+ rescue NoMethodError
198
+ nil
199
+ end
200
+ end
201
+
202
+ # Built-in validation rules
203
+ module Rules
204
+ # Required field validation
205
+ class Required < Rule
206
+ def validate(element, _context = {})
207
+ field = options[:field]
208
+ value = get_value(element, field)
209
+
210
+ return [] unless value.nil?
211
+
212
+ ["#{field} is required"]
213
+ end
214
+
215
+ private
216
+
217
+ def get_value(element, field)
218
+ if element.is_a?(CoreModel::Base)
219
+ element.public_send(field) if element.class.attributes.key?(field)
220
+ else
221
+ element.public_send(field)
222
+ end
223
+ rescue NoMethodError
224
+ nil
225
+ end
226
+ end
227
+
228
+ # Type validation
229
+ class Type < Rule
230
+ def validate(element, _context = {})
231
+ field = options[:field]
232
+ expected_type = options[:type]
233
+ value = field_value(element, field)
234
+
235
+ return [] if value.nil? && !options[:required]
236
+ return [] if value.nil?
237
+
238
+ return ["#{field} must be #{expected_type.name}, got #{value.class.name}"] unless value.is_a?(expected_type)
239
+
240
+ []
241
+ end
242
+ end
243
+
244
+ # Length validation
245
+ class Length < Rule
246
+ def validate(element, _context = {})
247
+ field = options[:field]
248
+ value = field_value(element, field)
249
+
250
+ return [] if value.nil?
251
+
252
+ errors = []
253
+ length = value.is_a?(String) ? value.length : 0
254
+
255
+ errors << "#{field} must have at least #{options[:min]} characters/items" if options[:min] && length < options[:min]
256
+
257
+ errors << "#{field} must have at most #{options[:max]} characters/items" if options[:max] && length > options[:max]
258
+
259
+ errors
260
+ end
261
+ end
262
+
263
+ # Count validation for arrays/collections
264
+ class Count < Rule
265
+ def validate(element, _context = {})
266
+ field = options[:field]
267
+ value = field_value(element, field)
268
+
269
+ return [] if value.nil?
270
+
271
+ errors = []
272
+ count = value.is_a?(Enumerable) ? value.count : 0
273
+
274
+ errors << "#{field} must have at least #{options[:min]} items" if options[:min] && count < options[:min]
275
+
276
+ errors << "#{field} must have at most #{options[:max]} items" if options[:max] && count > options[:max]
277
+
278
+ errors
279
+ end
280
+ end
281
+
282
+ # Format validation with regex
283
+ class Format < Rule
284
+ def validate(element, _context = {})
285
+ field = options[:field]
286
+ pattern = options[:pattern]
287
+ value = field_value(element, field)
288
+
289
+ return [] if value.nil?
290
+
291
+ return ["#{field} has invalid format"] unless pattern.match?(value.to_s)
292
+
293
+ []
294
+ end
295
+ end
296
+
297
+ # Custom block validation
298
+ class Custom < Rule
299
+ def validate(element, context = {})
300
+ block = options[:block]
301
+ return [] unless block
302
+
303
+ result = block.call(element, context)
304
+ Array(result)
305
+ end
306
+ end
307
+ end
308
+
309
+ # Validation schema definition
310
+ class Schema
311
+ attr_reader :fields, :rules
312
+
313
+ # Define a new schema
314
+ #
315
+ # @yield Block for schema definition
316
+ # @return [Schema] The defined schema
317
+ def self.define(&block)
318
+ schema = new
319
+ schema.instance_eval(&block) if block
320
+ schema
321
+ end
322
+
323
+ def initialize
324
+ @fields = {}
325
+ @rules = []
326
+ end
327
+
328
+ # Define a required field
329
+ #
330
+ # @param name [Symbol] Field name
331
+ # @param type [Class, nil] Expected type
332
+ # @param options [Hash] Additional options
333
+ # @return [void]
334
+ def required(name, type: nil, **options)
335
+ @fields[name] = { required: true, type: type, **options }
336
+ end
337
+
338
+ # Define an optional field
339
+ #
340
+ # @param name [Symbol] Field name
341
+ # @param type [Class, nil] Expected type
342
+ # @param options [Hash] Additional options
343
+ # @return [void]
344
+ def optional(name, type: nil, **options)
345
+ @fields[name] = { required: false, type: type, **options }
346
+ end
347
+
348
+ # Add a custom validation rule
349
+ #
350
+ # @param name [Symbol] Rule name
351
+ # @yield Block for validation
352
+ # @return [void]
353
+ def rule(name, &block)
354
+ @rules << Rules::Custom.new(name, block: block)
355
+ end
356
+
357
+ # Add a pre-built rule
358
+ #
359
+ # @param rule [Rule] The rule to add
360
+ # @return [void]
361
+ def add_rule(rule)
362
+ @rules << rule
363
+ end
364
+
365
+ # Validate a document
366
+ #
367
+ # @param document [Object] Document to validate
368
+ # @return [Result] Validation result
369
+ def validate(document)
370
+ result = Result.new
371
+
372
+ # Validate fields
373
+ @fields.each do |name, config|
374
+ validate_field(document, name, config, result)
375
+ end
376
+
377
+ # Run custom rules
378
+ @rules.each do |rule|
379
+ errors = rule.validate(document, schema: self)
380
+ errors.each { |e| result.add_error(e, code: rule.name) }
381
+ end
382
+
383
+ result
384
+ end
385
+
386
+ private
387
+
388
+ def validate_field(document, name, config, result)
389
+ value = field_value(document, name)
390
+ path = name.to_s
391
+
392
+ # Check required
393
+ if config[:required] && value.nil?
394
+ result.add_error("#{name} is required", path: path, code: :required)
395
+ return
396
+ end
397
+
398
+ return if value.nil?
399
+
400
+ # Check type
401
+ if config[:type] && !value.is_a?(config[:type])
402
+ result.add_error(
403
+ "#{name} must be #{config[:type].name}, got #{value.class.name}",
404
+ path: path,
405
+ code: :type
406
+ )
407
+ end
408
+
409
+ # Check min_length
410
+ if config[:min_length] && value.is_a?(String) && (value.length < config[:min_length])
411
+ result.add_error(
412
+ "#{name} must have at least #{config[:min_length]} characters",
413
+ path: path,
414
+ code: :min_length
415
+ )
416
+ end
417
+
418
+ # Check max_length
419
+ if config[:max_length] && value.is_a?(String) && (value.length > config[:max_length])
420
+ result.add_error(
421
+ "#{name} must have at most #{config[:max_length]} characters",
422
+ path: path,
423
+ code: :max_length
424
+ )
425
+ end
426
+
427
+ # Check min_count
428
+ if config[:min_count] && value.is_a?(Enumerable) && (value.count < config[:min_count])
429
+ result.add_error(
430
+ "#{name} must have at least #{config[:min_count]} items",
431
+ path: path,
432
+ code: :min_count
433
+ )
434
+ end
435
+
436
+ # Check format
437
+ return unless config[:format].is_a?(Regexp) && !config[:format].match?(value.to_s)
438
+
439
+ result.add_error(
440
+ "#{name} has invalid format",
441
+ path: path,
442
+ code: :format
443
+ )
444
+ end
445
+
446
+ def field_value(document, field)
447
+ if document.is_a?(CoreModel::Base)
448
+ document.public_send(field) if document.class.attributes.key?(field)
449
+ else
450
+ document.public_send(field)
451
+ end
452
+ rescue NoMethodError
453
+ nil
454
+ end
455
+ end
456
+
457
+ # Schema generator from CoreModel types
458
+ #
459
+ # Automatically generates validation schemas from Lutaml::Model classes.
460
+ # This enables automatic validation based on model structure.
461
+ #
462
+ # @example Generate schema from CoreModel class
463
+ # schema = Coradoc::Validation::SchemaGenerator.generate(Coradoc::CoreModel::StructuralElement)
464
+ # result = schema.validate(document)
465
+ #
466
+ # @example Customize generated schema
467
+ # schema = Coradoc::Validation::SchemaGenerator.generate(
468
+ # Coradoc::CoreModel::Block,
469
+ # required: [:content],
470
+ # ignored: [:metadata]
471
+ # )
472
+ #
473
+ class SchemaGenerator
474
+ class << self
475
+ # Generate a validation schema from a CoreModel class
476
+ #
477
+ # @param model_class [Class] The CoreModel class to generate schema from
478
+ # @param required [Array<Symbol>] Attributes to mark as required
479
+ # @param ignored [Array<Symbol>] Attributes to skip in schema
480
+ # @param custom_rules [Hash] Additional validation rules per attribute
481
+ # @return [Schema] Generated validation schema
482
+ #
483
+ # @example Basic generation
484
+ # schema = SchemaGenerator.generate(Coradoc::CoreModel::Block)
485
+ #
486
+ # @example With required fields
487
+ # schema = SchemaGenerator.generate(
488
+ # Coradoc::CoreModel::Block,
489
+ # required: [:content, :delimiter_type]
490
+ # )
491
+ #
492
+ # @example With custom rules
493
+ # schema = SchemaGenerator.generate(
494
+ # Coradoc::CoreModel::StructuralElement,
495
+ # custom_rules: {
496
+ # level: { min: 1, max: 6 }
497
+ # }
498
+ # )
499
+ #
500
+ def generate(model_class, required: [], ignored: [], custom_rules: {})
501
+ return nil unless model_class.is_a?(Class) && model_class < CoreModel::Base
502
+
503
+ # Pre-compute attribute definitions before the schema block
504
+ attribute_defs = compute_attribute_definitions(
505
+ model_class, required, ignored, custom_rules
506
+ )
507
+
508
+ Schema.define do
509
+ attribute_defs.each do |name, type, options, is_required|
510
+ if is_required
511
+ required name, type: type, **options
512
+ else
513
+ optional name, type: type, **options
514
+ end
515
+ end
516
+ end
517
+ end
518
+
519
+ # Map Lutaml::Model type to Ruby class
520
+ #
521
+ # @param type [Symbol, Class] The Lutaml::Model type
522
+ # @return [Class, Array<Class>] Ruby class(es)
523
+ def map_type(type)
524
+ # Handle Lutaml::Model type classes by name
525
+ type_name = type.to_s
526
+
527
+ case type_name
528
+ when 'Lutaml::Model::Type::String'
529
+ String
530
+ when 'Lutaml::Model::Type::Integer'
531
+ Integer
532
+ when 'Lutaml::Model::Type::Float'
533
+ Float
534
+ when 'Lutaml::Model::Type::Boolean'
535
+ [TrueClass, FalseClass]
536
+ when 'Lutaml::Model::Type::Date'
537
+ Date
538
+ when 'Lutaml::Model::Type::Time'
539
+ Time
540
+ when 'Lutaml::Model::Type::DateTime'
541
+ Time
542
+ when 'Lutaml::Model::Type::Hash'
543
+ Hash
544
+ when 'Lutaml::Model::Type::Array'
545
+ Array
546
+ else
547
+ # For non-Lutaml types (like CoreModel::Base), return the type itself
548
+ type.is_a?(Class) ? type : Object
549
+ end
550
+ end
551
+
552
+ private
553
+
554
+ # Compute attribute definitions for the schema
555
+ #
556
+ # @return [Array<Array>] Array of [name, type, options, required] tuples
557
+ def compute_attribute_definitions(model_class, required, ignored, custom_rules)
558
+ model_class.attributes.filter_map do |name, attr|
559
+ next if ignored.include?(name)
560
+
561
+ type = collection?(attr) ? Array : map_type(attr.type)
562
+ options = build_options(attr, custom_rules[name])
563
+ # By default, all fields are optional unless explicitly required
564
+ is_required = required.include?(name)
565
+
566
+ [name, type, options, is_required]
567
+ end
568
+ end
569
+
570
+ # Check if attribute is a collection
571
+ #
572
+ # @param attr [Lutaml::Model::Attribute] The attribute
573
+ # @return [Boolean]
574
+ def collection?(attr)
575
+ attr.options[:collection] == true
576
+ end
577
+
578
+ # Build validation options for an attribute
579
+ #
580
+ # @param attr [Lutaml::Model::Attribute] The attribute
581
+ # @param custom [Hash, nil] Custom rules for this attribute
582
+ # @return [Hash] Validation options
583
+ def build_options(attr, custom = nil)
584
+ options = {}
585
+
586
+ # Add collection validation
587
+ options[:min_count] = 0 if collection?(attr)
588
+
589
+ # Merge custom rules
590
+ options.merge!(custom) if custom
591
+
592
+ options
593
+ end
594
+ end
595
+ end
596
+
597
+ # Module-level methods
598
+ class << self
599
+ # Define a validation schema
600
+ #
601
+ # @yield Schema definition block
602
+ # @return [Schema] The defined schema
603
+ def define(&block)
604
+ Schema.define(&block)
605
+ end
606
+
607
+ # Generate a validation schema from a CoreModel class
608
+ #
609
+ # @param model_class [Class] The CoreModel class
610
+ # @param options [Hash] Options passed to SchemaGenerator.generate
611
+ # @return [Schema] Generated validation schema
612
+ #
613
+ # @example
614
+ # schema = Coradoc::Validation.auto_schema(Coradoc::CoreModel::Block)
615
+ # result = schema.validate(document)
616
+ #
617
+ def auto_schema(model_class, **options)
618
+ SchemaGenerator.generate(model_class, **options)
619
+ end
620
+
621
+ # Validate a document with default schema
622
+ #
623
+ # @param document [Object] Document to validate
624
+ # @return [Result] Validation result
625
+ def validate(document)
626
+ default_schema.validate(document)
627
+ end
628
+
629
+ # Get the default validation schema
630
+ #
631
+ # @return [Schema]
632
+ def default_schema
633
+ @default_schema ||= Schema.define do
634
+ optional :id, type: String
635
+ optional :title, type: String
636
+ end
637
+ end
638
+
639
+ # Set the default validation schema
640
+ #
641
+ # @param schema [Schema] The schema to use
642
+ # @return [void]
643
+ attr_writer :default_schema
644
+ end
645
+ end
646
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Coradoc
4
- VERSION = "1.1.8"
4
+ VERSION = '2.0.12'
5
5
  end