facturx 1.0.0 → 1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -1
- data/DOCUMENTATION.md +271 -0
- data/README.md +67 -89
- data/lib/facturx/attach.rb +4 -20
- data/lib/facturx/error.rb +5 -2
- data/lib/facturx/generate.rb +4 -4
- data/lib/facturx/pdf/composer.rb +32 -0
- data/lib/facturx/profile.rb +1 -1
- data/lib/facturx/profiles.rb +1 -8
- data/lib/facturx/source_reader.rb +1 -1
- data/lib/facturx/validation.rb +61 -0
- data/lib/facturx/version.rb +1 -1
- data/lib/facturx/{conformance.rb → writer/tracker.rb} +53 -85
- data/lib/facturx/writer.rb +20 -12
- data/lib/facturx/xml/parser.rb +1 -1
- data/lib/facturx/xml/report_builder.rb +44 -0
- data/lib/facturx/xml/schema_registry.rb +22 -0
- data/lib/facturx/xml/schema_validator.rb +7 -2
- data/lib/facturx/xml/validator.rb +32 -0
- data/lib/facturx/xml/verifier.rb +16 -11
- data/lib/facturx.rb +15 -6
- data/rbi/facturx.rbi +701 -36
- metadata +8 -2
data/lib/facturx/profiles.rb
CHANGED
|
@@ -4,44 +4,37 @@ require_relative 'profile'
|
|
|
4
4
|
|
|
5
5
|
module Facturx
|
|
6
6
|
module Profiles
|
|
7
|
-
SPECIFICATION_VERSION = '1.09.2'
|
|
8
|
-
SCHEMA_ROOT = File.expand_path("schema/#{SPECIFICATION_VERSION}", __dir__).freeze
|
|
9
|
-
|
|
10
7
|
ALL = [
|
|
11
8
|
Profile.new(
|
|
12
9
|
id: :minimum,
|
|
13
10
|
guideline_urn: 'urn:factur-x.eu:1p0:minimum',
|
|
14
|
-
xsd_path: File.join(SCHEMA_ROOT, 'minimum/Factur-X_1.09.2_MINIMUM.xsd').freeze,
|
|
15
11
|
conformance_level: 'MINIMUM'
|
|
16
12
|
),
|
|
17
13
|
Profile.new(
|
|
18
14
|
id: :basic_wl,
|
|
19
15
|
guideline_urn: 'urn:factur-x.eu:1p0:basicwl',
|
|
20
|
-
xsd_path: File.join(SCHEMA_ROOT, 'basic-wl/Factur-X_1.09.2_BASICWL.xsd').freeze,
|
|
21
16
|
conformance_level: 'BASIC WL'
|
|
22
17
|
),
|
|
23
18
|
Profile.new(
|
|
24
19
|
id: :basic,
|
|
25
20
|
guideline_urn: 'urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic',
|
|
26
|
-
xsd_path: File.join(SCHEMA_ROOT, 'basic/Factur-X_1.09.2_BASIC.xsd').freeze,
|
|
27
21
|
conformance_level: 'BASIC'
|
|
28
22
|
),
|
|
29
23
|
Profile.new(
|
|
30
24
|
id: :en16931,
|
|
31
25
|
guideline_urn: 'urn:cen.eu:en16931:2017',
|
|
32
|
-
xsd_path: File.join(SCHEMA_ROOT, 'en16931/Factur-X_1.09.2_EN16931.xsd').freeze,
|
|
33
26
|
conformance_level: 'EN 16931'
|
|
34
27
|
),
|
|
35
28
|
Profile.new(
|
|
36
29
|
id: :extended,
|
|
37
30
|
guideline_urn: 'urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended',
|
|
38
|
-
xsd_path: File.join(SCHEMA_ROOT, 'extended/Factur-X_1.09.2_EXTENDED.xsd').freeze,
|
|
39
31
|
conformance_level: 'EXTENDED'
|
|
40
32
|
)
|
|
41
33
|
].freeze
|
|
42
34
|
|
|
43
35
|
BY_ID = ALL.to_h { |profile| [profile.id, profile] }.freeze
|
|
44
36
|
BY_GUIDELINE_URN = ALL.to_h { |profile| [profile.guideline_urn, profile] }.freeze
|
|
37
|
+
private_constant :ALL, :BY_ID, :BY_GUIDELINE_URN
|
|
45
38
|
|
|
46
39
|
module_function
|
|
47
40
|
|
|
@@ -13,7 +13,7 @@ module Facturx
|
|
|
13
13
|
|
|
14
14
|
def call(source)
|
|
15
15
|
unless source.is_a?(String)
|
|
16
|
-
raise
|
|
16
|
+
raise InvalidSourceError.new('Source must be provided as a byte String', input_class: source.class.name)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
return Result.new(xml: source, source_type: :xml) unless pdf?(source)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'error'
|
|
4
|
+
require_relative 'model/immutable'
|
|
5
|
+
|
|
6
|
+
module Facturx
|
|
7
|
+
module Validation
|
|
8
|
+
LAYERS = %i[syntax profile document xsd].freeze
|
|
9
|
+
SEVERITIES = %i[error warning].freeze
|
|
10
|
+
ISSUE_DEFAULTS = {
|
|
11
|
+
severity: :error,
|
|
12
|
+
term_id: nil,
|
|
13
|
+
group_id: nil,
|
|
14
|
+
path: nil,
|
|
15
|
+
line: nil,
|
|
16
|
+
column: nil,
|
|
17
|
+
details: {}
|
|
18
|
+
}.freeze
|
|
19
|
+
private_constant :LAYERS, :SEVERITIES, :ISSUE_DEFAULTS
|
|
20
|
+
|
|
21
|
+
Issue = Data.define(:code, :message, :layer, :severity, :term_id, :group_id, :path, :line, :column, :details) do
|
|
22
|
+
include Model::ValidatedWith
|
|
23
|
+
|
|
24
|
+
def initialize(**attributes)
|
|
25
|
+
Model.validate_attributes!(self.class.members, attributes)
|
|
26
|
+
attributes.fetch_values(:code, :message, :layer)
|
|
27
|
+
values = ISSUE_DEFAULTS.merge(attributes)
|
|
28
|
+
validate_values!(values)
|
|
29
|
+
|
|
30
|
+
super(**values.transform_values { |value| Model.copy_and_freeze(value) })
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def validate_values!(values)
|
|
36
|
+
unless LAYERS.include?(values[:layer])
|
|
37
|
+
raise ArgumentError, "Unknown validation layer: #{values[:layer].inspect}"
|
|
38
|
+
end
|
|
39
|
+
unless SEVERITIES.include?(values[:severity])
|
|
40
|
+
raise ArgumentError, "Unknown validation severity: #{values[:severity].inspect}"
|
|
41
|
+
end
|
|
42
|
+
raise TypeError, 'details must be a Hash' unless values[:details].is_a?(Hash)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
Report = Data.define(:profile, :issues) do
|
|
47
|
+
include Model::ValidatedWith
|
|
48
|
+
|
|
49
|
+
def initialize(profile: nil, issues: [])
|
|
50
|
+
raise TypeError, 'issues must be an Array' unless issues.is_a?(Array)
|
|
51
|
+
raise TypeError, 'issues must contain validation issues' unless issues.all?(Issue)
|
|
52
|
+
|
|
53
|
+
super(profile: Model.copy_and_freeze(profile), issues: Model.copy_and_freeze(issues))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def valid? = issues.none? { |issue| issue.severity == :error }
|
|
57
|
+
|
|
58
|
+
def invalid? = !valid?
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/facturx/version.rb
CHANGED
|
@@ -1,38 +1,69 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'error'
|
|
4
|
-
require_relative '
|
|
3
|
+
require_relative '../error'
|
|
4
|
+
require_relative '../validation'
|
|
5
5
|
|
|
6
6
|
module Facturx
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
class Writer
|
|
8
|
+
class Tracker
|
|
9
|
+
attr_reader :profile
|
|
10
|
+
|
|
11
|
+
def initialize(profile:)
|
|
12
|
+
@profile = profile
|
|
13
|
+
@issues = []
|
|
14
|
+
end
|
|
10
15
|
|
|
11
|
-
def
|
|
12
|
-
|
|
13
|
-
attributes.fetch_values(:code, :message)
|
|
14
|
-
values = { term_id: nil, group_id: nil, path: nil, details: {} }.merge(attributes)
|
|
15
|
-
raise TypeError, 'details must be a Hash' unless values[:details].is_a?(Hash)
|
|
16
|
+
def check_guideline(guideline_urn, path: nil)
|
|
17
|
+
return if guideline_urn.nil? || guideline_urn == profile.guideline_urn
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
add(
|
|
20
|
+
:profile_mismatch,
|
|
21
|
+
'Document guideline does not match the selected profile',
|
|
22
|
+
term_id: 'BT-24',
|
|
23
|
+
path:,
|
|
24
|
+
details: { expected: profile.guideline_urn, actual: guideline_urn }
|
|
25
|
+
)
|
|
18
26
|
end
|
|
19
|
-
end
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
def check_document(document, path: nil)
|
|
29
|
+
check_guideline(document.guideline_urn, path:)
|
|
30
|
+
end
|
|
23
31
|
|
|
24
|
-
def
|
|
25
|
-
|
|
32
|
+
def observe_group?(group, count:, path: nil)
|
|
33
|
+
definition_observed?(group, count, path)
|
|
34
|
+
end
|
|
26
35
|
|
|
27
|
-
|
|
36
|
+
def observe_term?(term, values:, path: nil)
|
|
37
|
+
definition_observed?(term, values.size, path)
|
|
28
38
|
end
|
|
29
39
|
|
|
30
|
-
def
|
|
40
|
+
def invalid_term(term, error:, path: nil)
|
|
41
|
+
raise TypeError, 'error must be a FormattingError' unless error.is_a?(FormattingError)
|
|
31
42
|
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
add(
|
|
44
|
+
:invalid_value,
|
|
45
|
+
error.message,
|
|
46
|
+
term_id: term.id,
|
|
47
|
+
group_id: term.group_id,
|
|
48
|
+
path: path || error.details[:path] || term.xpath,
|
|
49
|
+
details: error.details
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def unrepresentable_attribute(group, model:, attribute:, path: nil)
|
|
54
|
+
add(
|
|
55
|
+
:unrepresentable_attribute,
|
|
56
|
+
'Model attribute cannot be represented in this group',
|
|
57
|
+
group_id: group.id,
|
|
58
|
+
path: path || group.xpath,
|
|
59
|
+
details: { model:, attribute: }
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def report
|
|
64
|
+
Validation::Report.new(profile:, issues: @issues)
|
|
65
|
+
end
|
|
34
66
|
|
|
35
|
-
module Tracking
|
|
36
67
|
private
|
|
37
68
|
|
|
38
69
|
def definition_observed?(definition, count, path)
|
|
@@ -77,7 +108,7 @@ module Facturx
|
|
|
77
108
|
end
|
|
78
109
|
|
|
79
110
|
def add(code, message, **context)
|
|
80
|
-
@issues << Issue.new(code:, message:, **context)
|
|
111
|
+
@issues << Validation::Issue.new(code:, message:, layer: :document, **context)
|
|
81
112
|
end
|
|
82
113
|
|
|
83
114
|
def definition_context(definition, path)
|
|
@@ -96,68 +127,5 @@ module Facturx
|
|
|
96
127
|
|
|
97
128
|
def repeated?(cardinality) = cardinality.end_with?('n')
|
|
98
129
|
end
|
|
99
|
-
private_constant :Tracking
|
|
100
|
-
|
|
101
|
-
class Tracker
|
|
102
|
-
include Tracking
|
|
103
|
-
|
|
104
|
-
attr_reader :profile
|
|
105
|
-
|
|
106
|
-
def initialize(profile:)
|
|
107
|
-
@profile = profile
|
|
108
|
-
@issues = []
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def check_guideline(guideline_urn, path: nil)
|
|
112
|
-
return if guideline_urn.nil? || guideline_urn == profile.guideline_urn
|
|
113
|
-
|
|
114
|
-
add(
|
|
115
|
-
:profile_mismatch,
|
|
116
|
-
'Document guideline does not match the selected profile',
|
|
117
|
-
term_id: 'BT-24',
|
|
118
|
-
path:,
|
|
119
|
-
details: { expected: profile.guideline_urn, actual: guideline_urn }
|
|
120
|
-
)
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def check_document(document, path: nil)
|
|
124
|
-
check_guideline(document.guideline_urn, path:)
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def observe_group?(group, count:, path: nil)
|
|
128
|
-
definition_observed?(group, count, path)
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def observe_term?(term, values:, path: nil)
|
|
132
|
-
definition_observed?(term, values.size, path)
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def invalid_term(term, error:, path: nil)
|
|
136
|
-
raise TypeError, 'error must be a FormattingError' unless error.is_a?(FormattingError)
|
|
137
|
-
|
|
138
|
-
add(
|
|
139
|
-
:invalid_value,
|
|
140
|
-
error.message,
|
|
141
|
-
term_id: term.id,
|
|
142
|
-
group_id: term.group_id,
|
|
143
|
-
path: path || error.details[:path] || term.xpath,
|
|
144
|
-
details: error.details
|
|
145
|
-
)
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def unrepresentable_attribute(group, model:, attribute:, path: nil)
|
|
149
|
-
add(
|
|
150
|
-
:unrepresentable_attribute,
|
|
151
|
-
'Model attribute cannot be represented in this group',
|
|
152
|
-
group_id: group.id,
|
|
153
|
-
path: path || group.xpath,
|
|
154
|
-
details: { model:, attribute: }
|
|
155
|
-
)
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def report
|
|
159
|
-
Report.new(profile:, issues: @issues)
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
130
|
end
|
|
163
131
|
end
|
data/lib/facturx/writer.rb
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'conformance'
|
|
4
3
|
require_relative 'error'
|
|
5
4
|
require_relative 'terms'
|
|
6
5
|
require_relative 'writer/context'
|
|
7
6
|
require_relative 'writer/stage'
|
|
7
|
+
require_relative 'writer/tracker'
|
|
8
8
|
require_relative 'writer/stages/document_context'
|
|
9
9
|
require_relative 'writer/stages/exchanged_document'
|
|
10
10
|
require_relative 'writer/stages/trade_lines'
|
|
@@ -12,6 +12,7 @@ require_relative 'writer/stages/trade_agreement'
|
|
|
12
12
|
require_relative 'writer/stages/trade_delivery'
|
|
13
13
|
require_relative 'writer/stages/trade_settlement'
|
|
14
14
|
require_relative 'xml/schema_validator'
|
|
15
|
+
require_relative 'xml/report_builder'
|
|
15
16
|
|
|
16
17
|
module Facturx
|
|
17
18
|
class Writer
|
|
@@ -29,37 +30,44 @@ module Facturx
|
|
|
29
30
|
raise 'Factur-X writer stages must cover every modeled term exactly once'
|
|
30
31
|
end
|
|
31
32
|
|
|
32
|
-
def initialize(schema_validator: Xml::SchemaValidator.new
|
|
33
|
+
def initialize(schema_validator: Xml::SchemaValidator.new)
|
|
33
34
|
@schema_validator = schema_validator
|
|
34
|
-
@tracker_class = tracker
|
|
35
|
-
@stages = stages.freeze
|
|
36
35
|
end
|
|
37
36
|
|
|
38
37
|
def call(document:, profile:)
|
|
39
38
|
context = compile(document:, profile:)
|
|
40
|
-
report = context
|
|
41
|
-
|
|
39
|
+
report = validate_context(context, profile)
|
|
40
|
+
raise_invalid_document(profile, report) if report.invalid?
|
|
42
41
|
|
|
43
|
-
@schema_validator.call(document: context.xml, profile:)
|
|
44
42
|
context.to_xml
|
|
45
43
|
end
|
|
46
44
|
|
|
47
45
|
def validate(document:, profile:)
|
|
48
|
-
compile(document:, profile:)
|
|
46
|
+
validate_context(compile(document:, profile:), profile)
|
|
49
47
|
end
|
|
50
48
|
|
|
51
49
|
private
|
|
52
50
|
|
|
53
51
|
def compile(document:, profile:)
|
|
54
|
-
tracker =
|
|
52
|
+
tracker = Tracker.new(profile:)
|
|
55
53
|
tracker.check_document(document, path: Terms.fetch('BT-24').xpath)
|
|
56
54
|
context = Context.new(document:, profile:, tracker:)
|
|
57
|
-
|
|
55
|
+
STAGES.each { |stage| stage.new(context).call }
|
|
58
56
|
context
|
|
59
57
|
end
|
|
60
58
|
|
|
61
|
-
def
|
|
62
|
-
|
|
59
|
+
def validate_context(context, profile)
|
|
60
|
+
report = context.tracker.report
|
|
61
|
+
return report if report.invalid?
|
|
62
|
+
|
|
63
|
+
@schema_validator.call(document: context.xml, profile:)
|
|
64
|
+
report
|
|
65
|
+
rescue XsdValidationError => e
|
|
66
|
+
Xml::ReportBuilder.xsd(profile, e)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def raise_invalid_document(profile, report)
|
|
70
|
+
raise InvalidDocumentError.new(
|
|
63
71
|
'Factur-X document does not conform to the selected profile',
|
|
64
72
|
profile: profile.id, report:, issues: report.issues
|
|
65
73
|
)
|
data/lib/facturx/xml/parser.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Facturx
|
|
|
8
8
|
class Parser
|
|
9
9
|
def call(xml:)
|
|
10
10
|
unless xml.is_a?(String)
|
|
11
|
-
raise
|
|
11
|
+
raise InvalidSourceError.new('XML must be provided as a byte String', input_class: xml.class.name)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
Nokogiri::XML::Document.parse(xml) { |config| config.strict.nonet }
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../validation'
|
|
4
|
+
|
|
5
|
+
module Facturx
|
|
6
|
+
module Xml
|
|
7
|
+
class ReportBuilder
|
|
8
|
+
class << self
|
|
9
|
+
def syntax(error)
|
|
10
|
+
from_error(error, layer: :syntax, code: :invalid_xml)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def profile(error)
|
|
14
|
+
guideline_urn = error.details[:guideline_urn]
|
|
15
|
+
code = guideline_urn.nil? || guideline_urn.empty? ? :missing_profile : :unknown_profile
|
|
16
|
+
from_error(error, layer: :profile, code:)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def xsd(profile, error)
|
|
20
|
+
from_error(error, profile:, layer: :xsd, code: :xsd_violation)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def from_error(error, layer:, code:, profile: nil)
|
|
26
|
+
diagnostics = error.details[:errors] || [{ message: error.message }]
|
|
27
|
+
issues = diagnostics.map { |diagnostic| issue(error, diagnostic, layer:, code:) }
|
|
28
|
+
Validation::Report.new(profile:, issues:)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def issue(error, diagnostic, layer:, code:)
|
|
32
|
+
Validation::Issue.new(
|
|
33
|
+
code:,
|
|
34
|
+
message: diagnostic.fetch(:message),
|
|
35
|
+
layer:,
|
|
36
|
+
line: diagnostic[:line],
|
|
37
|
+
column: diagnostic[:column],
|
|
38
|
+
details: error.details.except(:errors).merge(diagnostic.except(:message, :line, :column))
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Facturx
|
|
4
|
+
module Xml
|
|
5
|
+
class SchemaRegistry
|
|
6
|
+
SPECIFICATION_VERSION = '1.09.2'
|
|
7
|
+
ROOT = File.expand_path("../schema/#{SPECIFICATION_VERSION}", __dir__).freeze
|
|
8
|
+
PATHS = {
|
|
9
|
+
minimum: 'minimum/Factur-X_1.09.2_MINIMUM.xsd',
|
|
10
|
+
basic_wl: 'basic-wl/Factur-X_1.09.2_BASICWL.xsd',
|
|
11
|
+
basic: 'basic/Factur-X_1.09.2_BASIC.xsd',
|
|
12
|
+
en16931: 'en16931/Factur-X_1.09.2_EN16931.xsd',
|
|
13
|
+
extended: 'extended/Factur-X_1.09.2_EXTENDED.xsd'
|
|
14
|
+
}.transform_values { |path| File.join(ROOT, path).freeze }.freeze
|
|
15
|
+
private_constant :SPECIFICATION_VERSION, :ROOT, :PATHS
|
|
16
|
+
|
|
17
|
+
def fetch(profile)
|
|
18
|
+
PATHS.fetch(profile.id)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'nokogiri'
|
|
4
4
|
require_relative '../error'
|
|
5
|
+
require_relative 'schema_registry'
|
|
5
6
|
|
|
6
7
|
module Facturx
|
|
7
8
|
module Xml
|
|
@@ -27,6 +28,10 @@ module Facturx
|
|
|
27
28
|
end
|
|
28
29
|
end
|
|
29
30
|
|
|
31
|
+
def initialize(registry: SchemaRegistry.new)
|
|
32
|
+
@registry = registry
|
|
33
|
+
end
|
|
34
|
+
|
|
30
35
|
def call(document:, profile:)
|
|
31
36
|
validation_errors = load_schema(profile).validate(document)
|
|
32
37
|
raise_validation_error(profile, validation_errors) unless validation_errors.empty?
|
|
@@ -35,7 +40,7 @@ module Facturx
|
|
|
35
40
|
private
|
|
36
41
|
|
|
37
42
|
def load_schema(profile)
|
|
38
|
-
self.class.send(:load_schema,
|
|
43
|
+
self.class.send(:load_schema, @registry.fetch(profile))
|
|
39
44
|
rescue SystemCallError, Nokogiri::XML::SyntaxError => e
|
|
40
45
|
raise_schema_load_error(profile, e)
|
|
41
46
|
end
|
|
@@ -53,7 +58,7 @@ module Facturx
|
|
|
53
58
|
end
|
|
54
59
|
|
|
55
60
|
def raise_schema_load_error(profile, error)
|
|
56
|
-
raise
|
|
61
|
+
raise SchemaLoadError.new(
|
|
57
62
|
"Unable to load the Factur-X #{profile.id} schema",
|
|
58
63
|
profile: profile.id,
|
|
59
64
|
errors: [{ message: error.message }.freeze].freeze
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../validation'
|
|
4
|
+
require_relative 'parser'
|
|
5
|
+
require_relative 'profile_detector'
|
|
6
|
+
require_relative 'report_builder'
|
|
7
|
+
require_relative 'schema_validator'
|
|
8
|
+
|
|
9
|
+
module Facturx
|
|
10
|
+
module Xml
|
|
11
|
+
class Validator
|
|
12
|
+
def initialize(parser: Parser.new, profile_detector: ProfileDetector.new, schema_validator: SchemaValidator.new)
|
|
13
|
+
@parser = parser
|
|
14
|
+
@profile_detector = profile_detector
|
|
15
|
+
@schema_validator = schema_validator
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(xml:)
|
|
19
|
+
document = @parser.call(xml:)
|
|
20
|
+
profile = @profile_detector.call(document:)
|
|
21
|
+
@schema_validator.call(document:, profile:)
|
|
22
|
+
Validation::Report.new(profile:)
|
|
23
|
+
rescue UnknownProfileError => e
|
|
24
|
+
ReportBuilder.profile(e)
|
|
25
|
+
rescue XsdValidationError => e
|
|
26
|
+
ReportBuilder.xsd(profile, e)
|
|
27
|
+
rescue InvalidXmlError => e
|
|
28
|
+
ReportBuilder.syntax(e)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/facturx/xml/verifier.rb
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '
|
|
4
|
-
require_relative 'profile_detector'
|
|
5
|
-
require_relative 'schema_validator'
|
|
3
|
+
require_relative 'validator'
|
|
6
4
|
|
|
7
5
|
module Facturx
|
|
8
6
|
module Xml
|
|
9
7
|
class Verifier
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
ERROR_CLASSES = {
|
|
9
|
+
syntax: InvalidXmlError,
|
|
10
|
+
profile: UnknownProfileError,
|
|
11
|
+
xsd: XsdValidationError
|
|
12
|
+
}.freeze
|
|
13
|
+
private_constant :ERROR_CLASSES
|
|
14
|
+
|
|
15
|
+
def initialize(validator: Validator.new)
|
|
16
|
+
@validator = validator
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
def call(xml:)
|
|
17
|
-
|
|
18
|
-
profile
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
report = @validator.call(xml:)
|
|
21
|
+
return report.profile if report.valid?
|
|
22
|
+
|
|
23
|
+
issue = report.issues.first
|
|
24
|
+
error_class = ERROR_CLASSES.fetch(issue.layer)
|
|
25
|
+
raise error_class.new(issue.message, report:, issues: report.issues)
|
|
21
26
|
end
|
|
22
27
|
end
|
|
23
28
|
end
|
data/lib/facturx.rb
CHANGED
|
@@ -16,12 +16,14 @@ require 'facturx/diagnostic'
|
|
|
16
16
|
require 'facturx/reading'
|
|
17
17
|
require 'facturx/coerce'
|
|
18
18
|
require 'facturx/format'
|
|
19
|
-
require 'facturx/
|
|
19
|
+
require 'facturx/validation'
|
|
20
|
+
require 'facturx/xml/validator'
|
|
20
21
|
require 'facturx/xml/verifier'
|
|
21
22
|
require 'facturx/pdf/document'
|
|
22
23
|
require 'facturx/pdf/inspector'
|
|
23
24
|
require 'facturx/pdf/extractor'
|
|
24
25
|
require 'facturx/pdf/verifier'
|
|
26
|
+
require 'facturx/pdf/composer'
|
|
25
27
|
require 'facturx/composers/ghostscript'
|
|
26
28
|
require 'facturx/attach'
|
|
27
29
|
require 'facturx/reader'
|
|
@@ -29,12 +31,16 @@ require 'facturx/writer'
|
|
|
29
31
|
require 'facturx/generate'
|
|
30
32
|
|
|
31
33
|
module Facturx
|
|
32
|
-
|
|
34
|
+
DEFAULT_XML_VALIDATOR = Xml::Validator.new
|
|
35
|
+
DEFAULT_PDF_COMPOSER = Pdf::Composer.new
|
|
36
|
+
DEFAULT_ATTACHER = Attach.new(pdf_composer: DEFAULT_PDF_COMPOSER)
|
|
33
37
|
DEFAULT_READER = Reader.new
|
|
34
38
|
DEFAULT_PROFILE_RESOLVER = ProfileResolver.new
|
|
35
39
|
DEFAULT_WRITER = Writer.new
|
|
36
|
-
DEFAULT_GENERATOR = Generate.new(writer: DEFAULT_WRITER,
|
|
40
|
+
DEFAULT_GENERATOR = Generate.new(writer: DEFAULT_WRITER, pdf_composer: DEFAULT_PDF_COMPOSER,
|
|
37
41
|
profile_resolver: DEFAULT_PROFILE_RESOLVER)
|
|
42
|
+
private_constant :DEFAULT_XML_VALIDATOR
|
|
43
|
+
private_constant :DEFAULT_PDF_COMPOSER
|
|
38
44
|
private_constant :DEFAULT_ATTACHER
|
|
39
45
|
private_constant :DEFAULT_READER
|
|
40
46
|
private_constant :DEFAULT_PROFILE_RESOLVER
|
|
@@ -42,9 +48,8 @@ module Facturx
|
|
|
42
48
|
private_constant :DEFAULT_GENERATOR
|
|
43
49
|
|
|
44
50
|
class << self
|
|
45
|
-
def
|
|
46
|
-
|
|
47
|
-
true
|
|
51
|
+
def validate_xml(xml:)
|
|
52
|
+
DEFAULT_XML_VALIDATOR.call(xml:)
|
|
48
53
|
end
|
|
49
54
|
|
|
50
55
|
def attach(pdf:, xml:)
|
|
@@ -73,4 +78,8 @@ module Facturx
|
|
|
73
78
|
DEFAULT_GENERATOR.call(pdf:, document:, profile:)
|
|
74
79
|
end
|
|
75
80
|
end
|
|
81
|
+
|
|
82
|
+
private_constant :Attach, :Builders, :Coerce, :CoercionError, :Composers, :Format, :FormattingError,
|
|
83
|
+
:Generate, :Group, :Model, :Pdf, :ProfileResolver, :Reader, :SourceReader, :Term,
|
|
84
|
+
:TermDeclarations, :Terms, :Writer, :Xml
|
|
76
85
|
end
|