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.
- checksums.yaml +4 -4
- data/.rspec +1 -1
- data/Rakefile +3 -12
- data/exe/coradoc +21 -2
- data/lib/coradoc/cli.rb +185 -91
- data/lib/coradoc/configurable.rb +527 -0
- data/lib/coradoc/coradoc.rb +463 -0
- data/lib/coradoc/core_model/annotation_block.rb +57 -0
- data/lib/coradoc/core_model/base.rb +172 -0
- data/lib/coradoc/core_model/bibliography.rb +41 -0
- data/lib/coradoc/core_model/bibliography_entry.rb +48 -0
- data/lib/coradoc/core_model/block.rb +63 -0
- data/lib/coradoc/core_model/children_content.rb +53 -0
- data/lib/coradoc/core_model/comment_block.rb +10 -0
- data/lib/coradoc/core_model/definition_item.rb +46 -0
- data/lib/coradoc/core_model/definition_list.rb +28 -0
- data/lib/coradoc/core_model/element_attribute.rb +26 -0
- data/lib/coradoc/core_model/example_block.rb +10 -0
- data/lib/coradoc/core_model/footnote.rb +92 -0
- data/lib/coradoc/core_model/horizontal_rule_block.rb +10 -0
- data/lib/coradoc/core_model/id_generator.rb +16 -0
- data/lib/coradoc/core_model/image.rb +66 -0
- data/lib/coradoc/core_model/inline_element.rb +140 -0
- data/lib/coradoc/core_model/list_block.rb +135 -0
- data/lib/coradoc/core_model/list_item.rb +142 -0
- data/lib/coradoc/core_model/listing_block.rb +13 -0
- data/lib/coradoc/core_model/literal_block.rb +10 -0
- data/lib/coradoc/core_model/metadata.rb +79 -0
- data/lib/coradoc/core_model/open_block.rb +10 -0
- data/lib/coradoc/core_model/paragraph_block.rb +10 -0
- data/lib/coradoc/core_model/pass_block.rb +10 -0
- data/lib/coradoc/core_model/quote_block.rb +12 -0
- data/lib/coradoc/core_model/reviewer_block.rb +10 -0
- data/lib/coradoc/core_model/sidebar_block.rb +10 -0
- data/lib/coradoc/core_model/source_block.rb +10 -0
- data/lib/coradoc/core_model/structural_element.rb +94 -0
- data/lib/coradoc/core_model/table.rb +148 -0
- data/lib/coradoc/core_model/term.rb +53 -0
- data/lib/coradoc/core_model/text_content.rb +22 -0
- data/lib/coradoc/core_model/toc.rb +105 -0
- data/lib/coradoc/core_model/toc_generator.rb +151 -0
- data/lib/coradoc/core_model/verse_block.rb +12 -0
- data/lib/coradoc/core_model.rb +77 -0
- data/lib/coradoc/document_builder.rb +184 -0
- data/lib/coradoc/document_manipulator.rb +203 -0
- data/lib/coradoc/errors.rb +312 -0
- data/lib/coradoc/format_module.rb +49 -0
- data/lib/coradoc/hooks.rb +176 -0
- data/lib/coradoc/input.rb +17 -7
- data/lib/coradoc/logger.rb +54 -0
- data/lib/coradoc/output.rb +17 -6
- data/lib/coradoc/performance_regression.rb +109 -0
- data/lib/coradoc/processor_registry.rb +50 -0
- data/lib/coradoc/query.rb +455 -0
- data/lib/coradoc/registry.rb +156 -0
- data/lib/coradoc/serializer/registry.rb +150 -0
- data/lib/coradoc/transform.rb +11 -0
- data/lib/coradoc/validation.rb +646 -0
- data/lib/coradoc/version.rb +1 -1
- data/lib/coradoc/visitor.rb +283 -0
- data/lib/coradoc.rb +40 -19
- metadata +67 -277
- data/.editorconfig +0 -15
- data/.envrc +0 -1
- data/.irbrc +0 -1
- data/.pryrc.sample +0 -1
- data/.rubocop.yml +0 -14
- data/.rubocop_todo.yml +0 -179
- data/CHANGELOG.md +0 -9
- data/CODE_OF_CONDUCT.md +0 -84
- data/Dockerfile +0 -19
- data/Gemfile +0 -16
- data/LICENSE.txt +0 -21
- data/Makefile +0 -35
- data/README.Docker.adoc +0 -57
- data/README.adoc +0 -119
- data/coradoc.gemspec +0 -40
- data/docker-compose.yml +0 -14
- data/exe/reverse_adoc +0 -81
- data/exe/w2a +0 -60
- data/flake.lock +0 -114
- data/flake.nix +0 -135
- data/lib/coradoc/converter.rb +0 -144
- data/lib/coradoc/document.rb +0 -77
- data/lib/coradoc/element/admonition.rb +0 -18
- data/lib/coradoc/element/attribute.rb +0 -36
- data/lib/coradoc/element/attribute_list.rb +0 -138
- data/lib/coradoc/element/audio.rb +0 -33
- data/lib/coradoc/element/author.rb +0 -24
- data/lib/coradoc/element/base.rb +0 -92
- data/lib/coradoc/element/bibliography.rb +0 -24
- data/lib/coradoc/element/bibliography_entry.rb +0 -24
- data/lib/coradoc/element/block/core.rb +0 -76
- data/lib/coradoc/element/block/example.rb +0 -23
- data/lib/coradoc/element/block/listing.rb +0 -21
- data/lib/coradoc/element/block/literal.rb +0 -21
- data/lib/coradoc/element/block/open.rb +0 -22
- data/lib/coradoc/element/block/pass.rb +0 -21
- data/lib/coradoc/element/block/quote.rb +0 -19
- data/lib/coradoc/element/block/reviewer_comment.rb +0 -19
- data/lib/coradoc/element/block/side.rb +0 -19
- data/lib/coradoc/element/block/sourcecode.rb +0 -21
- data/lib/coradoc/element/block.rb +0 -17
- data/lib/coradoc/element/break.rb +0 -11
- data/lib/coradoc/element/comment_block.rb +0 -22
- data/lib/coradoc/element/comment_line.rb +0 -18
- data/lib/coradoc/element/document_attributes.rb +0 -33
- data/lib/coradoc/element/header.rb +0 -22
- data/lib/coradoc/element/image/block_image.rb +0 -32
- data/lib/coradoc/element/image/core.rb +0 -58
- data/lib/coradoc/element/image/inline_image.rb +0 -12
- data/lib/coradoc/element/image.rb +0 -10
- data/lib/coradoc/element/include.rb +0 -18
- data/lib/coradoc/element/inline/anchor.rb +0 -19
- data/lib/coradoc/element/inline/attribute_reference.rb +0 -19
- data/lib/coradoc/element/inline/bold.rb +0 -25
- data/lib/coradoc/element/inline/cross_reference.rb +0 -46
- data/lib/coradoc/element/inline/footnote.rb +0 -24
- data/lib/coradoc/element/inline/hard_line_break.rb +0 -11
- data/lib/coradoc/element/inline/highlight.rb +0 -25
- data/lib/coradoc/element/inline/italic.rb +0 -25
- data/lib/coradoc/element/inline/link.rb +0 -42
- data/lib/coradoc/element/inline/monospace.rb +0 -25
- data/lib/coradoc/element/inline/quotation.rb +0 -20
- data/lib/coradoc/element/inline/small.rb +0 -19
- data/lib/coradoc/element/inline/span.rb +0 -37
- data/lib/coradoc/element/inline/subscript.rb +0 -20
- data/lib/coradoc/element/inline/superscript.rb +0 -20
- data/lib/coradoc/element/inline/underline.rb +0 -19
- data/lib/coradoc/element/inline.rb +0 -23
- data/lib/coradoc/element/list/core.rb +0 -51
- data/lib/coradoc/element/list/definition.rb +0 -29
- data/lib/coradoc/element/list/ordered.rb +0 -17
- data/lib/coradoc/element/list/unordered.rb +0 -17
- data/lib/coradoc/element/list.rb +0 -13
- data/lib/coradoc/element/list_item.rb +0 -98
- data/lib/coradoc/element/list_item_definition.rb +0 -32
- data/lib/coradoc/element/paragraph.rb +0 -37
- data/lib/coradoc/element/revision.rb +0 -27
- data/lib/coradoc/element/section.rb +0 -62
- data/lib/coradoc/element/table.rb +0 -91
- data/lib/coradoc/element/tag.rb +0 -19
- data/lib/coradoc/element/term.rb +0 -22
- data/lib/coradoc/element/text_element.rb +0 -92
- data/lib/coradoc/element/title.rb +0 -62
- data/lib/coradoc/element/video.rb +0 -50
- data/lib/coradoc/generator.rb +0 -19
- data/lib/coradoc/input/adoc.rb +0 -30
- data/lib/coradoc/input/docx.rb +0 -64
- data/lib/coradoc/input/html/LICENSE.txt +0 -25
- data/lib/coradoc/input/html/README.adoc +0 -308
- data/lib/coradoc/input/html/cleaner.rb +0 -142
- data/lib/coradoc/input/html/config.rb +0 -77
- data/lib/coradoc/input/html/converters/a.rb +0 -52
- data/lib/coradoc/input/html/converters/aside.rb +0 -16
- data/lib/coradoc/input/html/converters/audio.rb +0 -29
- data/lib/coradoc/input/html/converters/base.rb +0 -108
- data/lib/coradoc/input/html/converters/blockquote.rb +0 -22
- data/lib/coradoc/input/html/converters/br.rb +0 -15
- data/lib/coradoc/input/html/converters/bypass.rb +0 -81
- data/lib/coradoc/input/html/converters/code.rb +0 -23
- data/lib/coradoc/input/html/converters/div.rb +0 -19
- data/lib/coradoc/input/html/converters/dl.rb +0 -62
- data/lib/coradoc/input/html/converters/drop.rb +0 -26
- data/lib/coradoc/input/html/converters/em.rb +0 -21
- data/lib/coradoc/input/html/converters/figure.rb +0 -25
- data/lib/coradoc/input/html/converters/h.rb +0 -42
- data/lib/coradoc/input/html/converters/head.rb +0 -23
- data/lib/coradoc/input/html/converters/hr.rb +0 -15
- data/lib/coradoc/input/html/converters/ignore.rb +0 -20
- data/lib/coradoc/input/html/converters/img.rb +0 -110
- data/lib/coradoc/input/html/converters/li.rb +0 -17
- data/lib/coradoc/input/html/converters/mark.rb +0 -19
- data/lib/coradoc/input/html/converters/markup.rb +0 -31
- data/lib/coradoc/input/html/converters/math.rb +0 -38
- data/lib/coradoc/input/html/converters/ol.rb +0 -65
- data/lib/coradoc/input/html/converters/p.rb +0 -23
- data/lib/coradoc/input/html/converters/pass_through.rb +0 -17
- data/lib/coradoc/input/html/converters/pre.rb +0 -55
- data/lib/coradoc/input/html/converters/q.rb +0 -16
- data/lib/coradoc/input/html/converters/strong.rb +0 -20
- data/lib/coradoc/input/html/converters/sub.rb +0 -22
- data/lib/coradoc/input/html/converters/sup.rb +0 -22
- data/lib/coradoc/input/html/converters/table.rb +0 -319
- data/lib/coradoc/input/html/converters/td.rb +0 -81
- data/lib/coradoc/input/html/converters/text.rb +0 -32
- data/lib/coradoc/input/html/converters/th.rb +0 -18
- data/lib/coradoc/input/html/converters/tr.rb +0 -22
- data/lib/coradoc/input/html/converters/video.rb +0 -29
- data/lib/coradoc/input/html/converters.rb +0 -59
- data/lib/coradoc/input/html/errors.rb +0 -14
- data/lib/coradoc/input/html/html_converter.rb +0 -168
- data/lib/coradoc/input/html/plugin.rb +0 -131
- data/lib/coradoc/input/html/plugins/plateau.rb +0 -213
- data/lib/coradoc/input/html/postprocessor.rb +0 -220
- data/lib/coradoc/input/html.rb +0 -61
- data/lib/coradoc/legacy_parser.rb +0 -200
- data/lib/coradoc/oscal.rb +0 -99
- data/lib/coradoc/output/adoc.rb +0 -19
- data/lib/coradoc/output/coradoc_tree_debug.rb +0 -21
- data/lib/coradoc/parser/asciidoc/admonition.rb +0 -24
- data/lib/coradoc/parser/asciidoc/attribute_list.rb +0 -89
- data/lib/coradoc/parser/asciidoc/base.rb +0 -87
- data/lib/coradoc/parser/asciidoc/bibliography.rb +0 -29
- data/lib/coradoc/parser/asciidoc/block.rb +0 -94
- data/lib/coradoc/parser/asciidoc/citation.rb +0 -30
- data/lib/coradoc/parser/asciidoc/content.rb +0 -64
- data/lib/coradoc/parser/asciidoc/document_attributes.rb +0 -25
- data/lib/coradoc/parser/asciidoc/header.rb +0 -29
- data/lib/coradoc/parser/asciidoc/inline.rb +0 -195
- data/lib/coradoc/parser/asciidoc/list.rb +0 -115
- data/lib/coradoc/parser/asciidoc/paragraph.rb +0 -54
- data/lib/coradoc/parser/asciidoc/section.rb +0 -61
- data/lib/coradoc/parser/asciidoc/table.rb +0 -32
- data/lib/coradoc/parser/asciidoc/term.rb +0 -41
- data/lib/coradoc/parser/asciidoc/text.rb +0 -158
- data/lib/coradoc/parser/base.rb +0 -40
- data/lib/coradoc/parser.rb +0 -11
- data/lib/coradoc/reverse_adoc.rb +0 -18
- data/lib/coradoc/transformer.rb +0 -476
- data/lib/coradoc/util.rb +0 -12
- data/lib/reverse_adoc.rb +0 -20
- data/utils/inspect_asciidoc.rb +0 -29
- data/utils/parser_analyzer.rb +0 -66
- 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
|
data/lib/coradoc/version.rb
CHANGED