lens_core 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.
- checksums.yaml +7 -0
- data/Gemfile +13 -0
- data/LICENSE +25 -0
- data/README.adoc +62 -0
- data/Rakefile +12 -0
- data/docs/diagnostics-v1.adoc +237 -0
- data/lens_core.gemspec +32 -0
- data/lib/lens_core/citation.rb +50 -0
- data/lib/lens_core/diagnostic.rb +66 -0
- data/lib/lens_core/errors.rb +48 -0
- data/lib/lens_core/location.rb +81 -0
- data/lib/lens_core/node.rb +59 -0
- data/lib/lens_core/parsers/json.rb +25 -0
- data/lib/lens_core/parsers/text.rb +41 -0
- data/lib/lens_core/parsers/yaml.rb +68 -0
- data/lib/lens_core/producer.rb +28 -0
- data/lib/lens_core/profile.rb +29 -0
- data/lib/lens_core/renderers/human.rb +85 -0
- data/lib/lens_core/renderers/json.rb +18 -0
- data/lib/lens_core/renderers/yaml.rb +16 -0
- data/lib/lens_core/report.rb +141 -0
- data/lib/lens_core/schema.rb +22 -0
- data/lib/lens_core/validation.rb +164 -0
- data/lib/lens_core/version.rb +10 -0
- data/lib/lens_core.rb +25 -0
- data/schemas/lens-diagnostics-v1.json +119 -0
- metadata +71 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: afb85b28397a4ddf5ce37a849ed0c86ca3a8cb5a0d31bf1d827f57d398135ac6
|
|
4
|
+
data.tar.gz: 4d617c41ea6d3bae184a0a18f9beb589a9cfdc78f0d80e89fbd6ecc480f7250d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 60c91a54b74fb9c0c3fae2c14c98b753a950377834752576b452f824db7cc0746c6cf0a9778be3eab3a1a698c677ff644d90b66c61d53c4dd73fa7028210c032
|
|
7
|
+
data.tar.gz: 6f3b1d77bc94d6995cb5e47528b1e5b6931b5fc91026e485e1b33b02f78c54e3a3d68e77a9c58a3d64210d77342b4c4f0a5bca48dd188148a1a701f7219a187d
|
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Ribose
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
17
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
18
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
19
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
20
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
21
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
22
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
23
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
24
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
25
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.adoc
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
= LensCore
|
|
2
|
+
|
|
3
|
+
LensCore is a small, dependency-free model for conformance diagnostics: one
|
|
4
|
+
report shape that any format checker can emit, with machine-readable (JSON,
|
|
5
|
+
YAML) and human-readable output rendered from the same model.
|
|
6
|
+
|
|
7
|
+
Each diagnostic records:
|
|
8
|
+
|
|
9
|
+
* what is wrong (`kind`);
|
|
10
|
+
* how serious it is (`severity`);
|
|
11
|
+
* where it is: line and column for text formats, byte offset and record for
|
|
12
|
+
binary ones;
|
|
13
|
+
* which rule of which specification or profile it breaks.
|
|
14
|
+
|
|
15
|
+
== Usage
|
|
16
|
+
|
|
17
|
+
[source,ruby]
|
|
18
|
+
----
|
|
19
|
+
require "lens_core"
|
|
20
|
+
|
|
21
|
+
report = LensCore::Report.new(
|
|
22
|
+
producer: LensCore::Producer.new(name: "my-checker", version: "1.0.0"),
|
|
23
|
+
profile: LensCore::Profile.new(id: "my-profile"),
|
|
24
|
+
valid: false,
|
|
25
|
+
diagnostics: [
|
|
26
|
+
LensCore::Diagnostic.new(
|
|
27
|
+
severity: "error", kind: "bad_checksum", requirement_id: "REC-001",
|
|
28
|
+
message: "Record checksum does not match.",
|
|
29
|
+
location: LensCore::Location.binary(byte_offset: 0, record_index: 0)
|
|
30
|
+
)
|
|
31
|
+
]
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
puts LensCore::Renderers::Json.render(report)
|
|
35
|
+
puts LensCore::Renderers::Human.render(report)
|
|
36
|
+
|
|
37
|
+
report = LensCore::Parsers::Json.parse(File.binread("report.json"))
|
|
38
|
+
----
|
|
39
|
+
|
|
40
|
+
Parsers and renderers are `LensCore::Parsers::Json`, `Parsers::Yaml` and
|
|
41
|
+
`LensCore::Renderers::Json`, `Renderers::Yaml`, `Renderers::Human`. Failures
|
|
42
|
+
are subclasses of `LensCore::Error`.
|
|
43
|
+
|
|
44
|
+
== Schema
|
|
45
|
+
|
|
46
|
+
The format is documented in link:docs/diagnostics-v1.adoc[docs/diagnostics-v1.adoc]
|
|
47
|
+
and specified by `schemas/lens-diagnostics-v1.json` (also at
|
|
48
|
+
`LensCore::Schema::PATH`). The gem version is `LensCore::VERSION`; the schema
|
|
49
|
+
version is `LensCore::SCHEMA_VERSION` (`1`).
|
|
50
|
+
|
|
51
|
+
== Status
|
|
52
|
+
|
|
53
|
+
The v1 contract is a ready-to-tag candidate: the schema, model, renderers and
|
|
54
|
+
the golden corpus (`spec/fixtures/v1/golden`) are pinned by SHA-256 in
|
|
55
|
+
`spec/fixtures/v1/golden/manifest.yml`. Nothing is tagged or published yet.
|
|
56
|
+
Adopt it by pinning an exact commit, never a branch; a host application that
|
|
57
|
+
needs only LensCore is shown in `spec/fixtures/host_app`.
|
|
58
|
+
|
|
59
|
+
== License
|
|
60
|
+
|
|
61
|
+
The gem is available as open source under the terms of the BSD 2-Clause
|
|
62
|
+
License.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
= LensCore diagnostics, schema version 1
|
|
2
|
+
:toc:
|
|
3
|
+
|
|
4
|
+
This document is the prose half of the diagnostics wire format. The machine
|
|
5
|
+
half is `schemas/lens-diagnostics-v1.json` (JSON Schema 2020-12). The Ruby
|
|
6
|
+
model in `lib/lens_core/` implements both. Where they differ, the difference
|
|
7
|
+
is listed under <<beyond-schema>>.
|
|
8
|
+
|
|
9
|
+
The gem version (`LensCore::VERSION`) and the schema version
|
|
10
|
+
(`LensCore::SCHEMA_VERSION`, also `LensCore::Schema.version`) are separate
|
|
11
|
+
constants. A host that embeds LensCore should report both.
|
|
12
|
+
|
|
13
|
+
== Document shape
|
|
14
|
+
|
|
15
|
+
A report is one JSON (or YAML) object. Members appear in the order shown.
|
|
16
|
+
|
|
17
|
+
[cols="2,1,4",options="header"]
|
|
18
|
+
|===
|
|
19
|
+
| Member | Required | Meaning
|
|
20
|
+
|
|
21
|
+
| `schema_version` | yes | The integer `1`. Written without a fraction.
|
|
22
|
+
| `producer` | yes | The tool that made the report: `name`, `version`
|
|
23
|
+
(non-empty strings).
|
|
24
|
+
| `profile` | yes | The rule set checked against: `id` (non-empty string) and
|
|
25
|
+
`citations` (array, may be empty).
|
|
26
|
+
| `timestamp` | no | When the check ran, if the caller says so. RFC 3339 with an
|
|
27
|
+
explicit offset. See <<timestamps>>.
|
|
28
|
+
| `valid` | yes | Boolean verdict, stated by the producer.
|
|
29
|
+
| `summary` | yes | `errors`, `warnings`, `infos`: integers derived from
|
|
30
|
+
`diagnostics`.
|
|
31
|
+
| `diagnostics` | yes | Array of diagnostics, in the producer's order (may be
|
|
32
|
+
empty).
|
|
33
|
+
|===
|
|
34
|
+
|
|
35
|
+
=== Diagnostic
|
|
36
|
+
|
|
37
|
+
[cols="2,1,4",options="header"]
|
|
38
|
+
|===
|
|
39
|
+
| Member | Required | Meaning
|
|
40
|
+
|
|
41
|
+
| `severity` | yes | `error`, `warning` or `info`.
|
|
42
|
+
| `kind` | yes | Non-empty string naming the sort of finding. Open
|
|
43
|
+
vocabulary: see <<compatibility>>.
|
|
44
|
+
| `requirement_id` | yes | Non-empty string identifying the requirement, stable
|
|
45
|
+
across releases of the producer.
|
|
46
|
+
| `message` | yes | Non-empty human-readable text. May contain newlines.
|
|
47
|
+
| `citations` | no | Array of citations; `[]` when absent.
|
|
48
|
+
| `remediation` | no | Non-empty text saying how to fix it.
|
|
49
|
+
| `context` | no | JSON object of supporting values; `{}` when absent.
|
|
50
|
+
| `location` | no | A location. Absent when the finding has no place.
|
|
51
|
+
|===
|
|
52
|
+
|
|
53
|
+
=== Citation
|
|
54
|
+
|
|
55
|
+
`uri` (required): an absolute URI, that is a scheme followed by `:` and
|
|
56
|
+
non-whitespace text (`https://...`, `urn:...`). `label` (optional): a
|
|
57
|
+
non-empty human name for the cited text.
|
|
58
|
+
|
|
59
|
+
=== Location
|
|
60
|
+
|
|
61
|
+
`type` is required and is `xml` or `binary`. The other members are
|
|
62
|
+
coordinates, all optional; a coordinate that is unknown is left out.
|
|
63
|
+
|
|
64
|
+
[cols="1,2,4",options="header"]
|
|
65
|
+
|===
|
|
66
|
+
| Type | Member | Meaning
|
|
67
|
+
|
|
68
|
+
| `xml` | `line` | Integer >= 1.
|
|
69
|
+
| `xml` | `column` | Integer >= 1.
|
|
70
|
+
| `xml` | `path` | Non-empty string, e.g. an element path.
|
|
71
|
+
| `binary` | `byte_offset` | Integer >= 0.
|
|
72
|
+
| `binary` | `byte_length` | Integer >= 0.
|
|
73
|
+
| `binary` | `record_index` | Integer >= 0, counting from zero.
|
|
74
|
+
| `binary` | `record_type` | Non-empty string.
|
|
75
|
+
|===
|
|
76
|
+
|
|
77
|
+
A coordinate that belongs to the other type (`line` on a `binary` location) is
|
|
78
|
+
a schema violation. The two types are format-neutral examples; neither implies
|
|
79
|
+
that any particular lens uses this gem.
|
|
80
|
+
|
|
81
|
+
[[compatibility]]
|
|
82
|
+
== Compatibility
|
|
83
|
+
|
|
84
|
+
* A document whose `schema_version` is any integer other than the one this gem
|
|
85
|
+
implements is rejected with `LensCore::UnsupportedVersionError`. A
|
|
86
|
+
`schema_version` that is not an integer is a `LensCore::SchemaError`.
|
|
87
|
+
* Within version 1, producers may add to a report. Consumers must accept:
|
|
88
|
+
- new `kind` values;
|
|
89
|
+
- additional (unknown) members on any object: the report, `producer`,
|
|
90
|
+
`profile`, a diagnostic, a citation, a location.
|
|
91
|
+
* The Ruby model keeps unknown members as _extensions_ and writes them back
|
|
92
|
+
after the standard members (see <<ordering>>). The one exception is
|
|
93
|
+
`summary`: its counts are derived, so any other member inside it is ignored
|
|
94
|
+
and not written back.
|
|
95
|
+
* Removing a member, changing a member's meaning or type, or adding a new
|
|
96
|
+
`severity` or location `type` is a new schema version.
|
|
97
|
+
* Integers must be written as integers (`3`, not `3.0` or `3e0`). JSON Schema
|
|
98
|
+
validators treat `3.0` as an integer; LensCore does not.
|
|
99
|
+
|
|
100
|
+
[[omission]]
|
|
101
|
+
== Omission and null
|
|
102
|
+
|
|
103
|
+
* An optional member that has no value is omitted. `null` is never a valid
|
|
104
|
+
value for a standard member; a document that contains one is a
|
|
105
|
+
`SchemaError`. (Extension members and values inside `context` may be `null`
|
|
106
|
+
like any JSON value.)
|
|
107
|
+
* `citations` (on a diagnostic) and `context` read as `[]` and `{}` when
|
|
108
|
+
absent. The renderers always write them, so canonical output has them.
|
|
109
|
+
* `timestamp`, `remediation` and `location` are written only when present.
|
|
110
|
+
* Location coordinates are never defaulted. A zero is written only when the
|
|
111
|
+
coordinate is really zero.
|
|
112
|
+
|
|
113
|
+
[[ordering]]
|
|
114
|
+
== Ordering and canonical form
|
|
115
|
+
|
|
116
|
+
* `diagnostics` keep the order given by the producer. Nothing sorts them.
|
|
117
|
+
* Standard members appear in the order of the tables above: report members as
|
|
118
|
+
`schema_version`, `producer`, `profile`, `timestamp`, `valid`, `summary`,
|
|
119
|
+
`diagnostics`; diagnostic members as `severity`, `kind`, `requirement_id`,
|
|
120
|
+
`message`, `citations`, `remediation`, `context`, `location`.
|
|
121
|
+
* Extension members follow the standard members, sorted lexically by bytes.
|
|
122
|
+
* Keys inside `context` and inside extension values are sorted lexically, at
|
|
123
|
+
every depth.
|
|
124
|
+
* `summary` is computed from the diagnostics: one count per severity.
|
|
125
|
+
* Canonical JSON is `JSON.pretty_generate` output (two-space indent) plus one
|
|
126
|
+
final newline. Parsing canonical JSON and rendering it again returns the same
|
|
127
|
+
bytes. Non-canonical JSON (other key order, other spacing) parses to the same
|
|
128
|
+
model and renders in canonical form.
|
|
129
|
+
* YAML carries the same content with the same key order, rendered with
|
|
130
|
+
`YAML.dump` and unbounded line width.
|
|
131
|
+
|
|
132
|
+
[[timestamps]]
|
|
133
|
+
== Timestamps
|
|
134
|
+
|
|
135
|
+
LensCore never reads the clock. A `timestamp` appears only when the caller
|
|
136
|
+
supplies one, as a `Time` or an RFC 3339 string with an explicit offset
|
|
137
|
+
(`Z` or `+hh:mm`). It is rendered in UTC with a `Z` suffix, with sub-second
|
|
138
|
+
digits only when there are some (`2026-10-23T10:00:00Z`,
|
|
139
|
+
`2026-10-23T10:00:00.25Z`). A string without an offset is rejected rather
|
|
140
|
+
than guessed at.
|
|
141
|
+
|
|
142
|
+
In YAML, quote the timestamp (the renderer does). An unquoted RFC 3339
|
|
143
|
+
timestamp is also accepted, in the `timestamp` member only.
|
|
144
|
+
|
|
145
|
+
== Validity
|
|
146
|
+
|
|
147
|
+
`valid` is the producer's verdict, because policy (do warnings fail a
|
|
148
|
+
document?) belongs to the producer. One rule is fixed: `valid` cannot be
|
|
149
|
+
`true` when the report contains an `error`.
|
|
150
|
+
|
|
151
|
+
== Text and encoding
|
|
152
|
+
|
|
153
|
+
* Documents are UTF-8. The parsers accept a String tagged UTF-8, US-ASCII or
|
|
154
|
+
binary and read its bytes as UTF-8; a String in any other encoding is
|
|
155
|
+
rejected, not transcoded.
|
|
156
|
+
* A byte order mark is rejected.
|
|
157
|
+
* Every string in a report, including object keys, must be valid UTF-8.
|
|
158
|
+
|
|
159
|
+
== Renderers
|
|
160
|
+
|
|
161
|
+
JSON::
|
|
162
|
+
`LensCore::Renderers::Json.render(report)`: canonical JSON.
|
|
163
|
+
YAML::
|
|
164
|
+
`LensCore::Renderers::Yaml.render(report)`: canonical YAML.
|
|
165
|
+
Human::
|
|
166
|
+
`LensCore::Renderers::Human.render(report)`: plain text with no colour or
|
|
167
|
+
wrapping. A header line (producer, profile, the timestamp if present), a
|
|
168
|
+
`VALID`/`INVALID` line with the three counts, then one block per diagnostic
|
|
169
|
+
in order, showing severity, requirement ID, kind, location (present
|
|
170
|
+
coordinates only), message, remediation, citations and context. It is meant
|
|
171
|
+
for reading, not parsing.
|
|
172
|
+
|
|
173
|
+
All three show every diagnostic of every severity.
|
|
174
|
+
|
|
175
|
+
== Parsers and failures
|
|
176
|
+
|
|
177
|
+
`LensCore::Parsers::Json.parse(text)` and `LensCore::Parsers::Yaml.parse(text)`
|
|
178
|
+
return a `LensCore::Report` or raise a subclass of `LensCore::Error`:
|
|
179
|
+
|
|
180
|
+
[cols="2,4",options="header"]
|
|
181
|
+
|===
|
|
182
|
+
| Error class | Raised when
|
|
183
|
+
|
|
184
|
+
| `LensCore::ParseError` | The text is not well-formed JSON or YAML, is not a
|
|
185
|
+
String, or starts with a byte order mark.
|
|
186
|
+
| `LensCore::UnsafeYamlError` | The YAML uses an object tag, a symbol, or an
|
|
187
|
+
alias. Anchor declarations that nothing aliases are harmless and accepted.
|
|
188
|
+
| `LensCore::InvalidEncodingError` | The input, or a string inside it, is not
|
|
189
|
+
valid UTF-8, or the input is a String in another encoding.
|
|
190
|
+
| `LensCore::UnsupportedVersionError` | `schema_version` is an integer other
|
|
191
|
+
than 1.
|
|
192
|
+
| `LensCore::SchemaError` | The document is well-formed but breaks the schema.
|
|
193
|
+
`error.path` is the list of keys and indexes to the offending member;
|
|
194
|
+
`error.message` renders it, e.g. `diagnostics[1].location.line: must be >= 1`.
|
|
195
|
+
|===
|
|
196
|
+
|
|
197
|
+
The same `SchemaError` and `InvalidEncodingError` are raised when a value
|
|
198
|
+
object is built directly in Ruby with bad arguments.
|
|
199
|
+
|
|
200
|
+
[[beyond-schema]]
|
|
201
|
+
== Rules beyond JSON Schema
|
|
202
|
+
|
|
203
|
+
JSON Schema validation alone accepts documents that LensCore rejects:
|
|
204
|
+
|
|
205
|
+
* `summary` counts must equal the number of diagnostics of each severity.
|
|
206
|
+
JSON Schema cannot count array items by property.
|
|
207
|
+
* Integer-valued numbers written with a fraction (`3.0`).
|
|
208
|
+
* Unsafe YAML constructs and invalid UTF-8, which are rejected before a
|
|
209
|
+
schema applies.
|
|
210
|
+
|
|
211
|
+
The schema does enforce that `valid: true` goes with `summary.errors` equal to
|
|
212
|
+
zero.
|
|
213
|
+
|
|
214
|
+
== Golden corpus
|
|
215
|
+
|
|
216
|
+
`spec/fixtures/v1/golden` holds canonical v1 reports that every release must
|
|
217
|
+
parse and render byte for byte, and that validate against the schema.
|
|
218
|
+
`manifest.yml` records, for each file, its SHA-256, producer and version,
|
|
219
|
+
schema version, source fixture and the LensCore contract commit used to render
|
|
220
|
+
it; SVG exports also record the exact svg_conform commit. The schema's own
|
|
221
|
+
SHA-256 is recorded too. Goldens omit timestamps.
|
|
222
|
+
|
|
223
|
+
* `svg_conform/` holds three real exports: an SVG 1.1 CSS grammar error, an
|
|
224
|
+
SVG 2 secure-static external reference, and a four-channel report (error,
|
|
225
|
+
validity error, warning, info).
|
|
226
|
+
* `examples/` holds format-neutral examples of the two location forms. The
|
|
227
|
+
binary example is a contract example only; it is not evidence of any PNG,
|
|
228
|
+
EMF or PostScript implementation.
|
|
229
|
+
|
|
230
|
+
`ruby -Ilib script/refresh_svg_goldens PATH_TO_SVG_CONFORM` regenerates the
|
|
231
|
+
three SVG files and their hashes. It refuses a checkout whose HEAD is not the
|
|
232
|
+
commit named in the manifest, canonicalizes through LensCore, and writes only
|
|
233
|
+
inside this repository.
|
|
234
|
+
|
|
235
|
+
`spec/fixtures/host_app` is a minimal application whose bundle contains only
|
|
236
|
+
LensCore. `bundle exec ruby read_reports.rb DIRECTORY` reads every report in a
|
|
237
|
+
directory and prints profile, counts and locations.
|
data/lens_core.gemspec
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/lens_core/version"
|
|
4
|
+
|
|
5
|
+
all_files_in_git = Dir.chdir(File.expand_path(__dir__)) do
|
|
6
|
+
`git ls-files -z`.split("\x0")
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
Gem::Specification.new do |spec|
|
|
10
|
+
spec.name = "lens_core"
|
|
11
|
+
spec.version = LensCore::VERSION
|
|
12
|
+
spec.authors = ["Ribose"]
|
|
13
|
+
spec.email = ["open.source@ribose.com"]
|
|
14
|
+
|
|
15
|
+
spec.summary = "Format-neutral conformance diagnostics model and renderers."
|
|
16
|
+
spec.description = "One diagnostics report shape (schema version 1) " \
|
|
17
|
+
"that any format checker can emit, with JSON and " \
|
|
18
|
+
"YAML parsers and JSON, YAML and human-readable " \
|
|
19
|
+
"renderers. No runtime dependencies."
|
|
20
|
+
spec.homepage = "https://github.com/claricle/lens_core"
|
|
21
|
+
spec.license = "BSD-2-Clause"
|
|
22
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.1.0")
|
|
23
|
+
|
|
24
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
25
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
26
|
+
spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
|
|
27
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
28
|
+
|
|
29
|
+
spec.files = all_files_in_git
|
|
30
|
+
.reject { |f| f.match(%r{\A(?:(?:test|spec|features|bin|script)/|\.)}) }
|
|
31
|
+
spec.require_paths = ["lib"]
|
|
32
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LensCore
|
|
4
|
+
# A pointer to the text a requirement comes from.
|
|
5
|
+
class Citation < Node
|
|
6
|
+
MEMBERS = %w[uri label].freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :uri, :label
|
|
9
|
+
|
|
10
|
+
def self.from_h(hash)
|
|
11
|
+
known, extra = split(hash)
|
|
12
|
+
new(uri: known["uri"], label: known["label"], extensions: extra)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Wire array -> frozen array of Citation; errors carry the array index.
|
|
16
|
+
def self.list_from_h(list, name = "citations")
|
|
17
|
+
Validation.array(list, name).each_with_index.map do |item, i|
|
|
18
|
+
from_h(item)
|
|
19
|
+
rescue SchemaError => e
|
|
20
|
+
raise e.within(i).within(name)
|
|
21
|
+
end.freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Ruby array -> frozen array, every item a Citation.
|
|
25
|
+
def self.check_list(list, name = "citations")
|
|
26
|
+
Validation.array(list, name).each_with_index.map do |item, i|
|
|
27
|
+
unless item.is_a?(self)
|
|
28
|
+
Validation.fail_at("must be a LensCore::Citation",
|
|
29
|
+
[name, i])
|
|
30
|
+
end
|
|
31
|
+
item
|
|
32
|
+
end.freeze
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param uri [String] absolute URI (required)
|
|
36
|
+
# @param label [String, nil] human-readable name
|
|
37
|
+
def initialize(uri:, label: nil, extensions: {})
|
|
38
|
+
@uri = Validation.absolute_uri(uri, "uri")
|
|
39
|
+
@label = label.nil? ? nil : Validation.string(label, "label")
|
|
40
|
+
super(extensions: extensions)
|
|
41
|
+
freeze
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def standard_members
|
|
47
|
+
{ "uri" => uri, "label" => label }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LensCore
|
|
4
|
+
# One finding. `kind` is an open vocabulary: producers may introduce new
|
|
5
|
+
# values, consumers must tolerate ones they do not know.
|
|
6
|
+
class Diagnostic < Node
|
|
7
|
+
SEVERITIES = %w[error warning info].freeze
|
|
8
|
+
MEMBERS = %w[severity kind requirement_id message citations remediation
|
|
9
|
+
context location].freeze
|
|
10
|
+
|
|
11
|
+
attr_reader(*MEMBERS.map(&:to_sym))
|
|
12
|
+
|
|
13
|
+
def self.from_h(hash)
|
|
14
|
+
known, extra = split(hash)
|
|
15
|
+
new(
|
|
16
|
+
severity: known["severity"], kind: known["kind"],
|
|
17
|
+
requirement_id: known["requirement_id"], message: known["message"],
|
|
18
|
+
remediation: known["remediation"], context: known.fetch("context", {}),
|
|
19
|
+
citations: Citation.list_from_h(known.fetch("citations", [])),
|
|
20
|
+
location: location_from(known["location"]), extensions: extra
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.location_from(value)
|
|
25
|
+
return nil if value.nil?
|
|
26
|
+
|
|
27
|
+
Location.from_h(value)
|
|
28
|
+
rescue SchemaError => e
|
|
29
|
+
raise e.within("location")
|
|
30
|
+
end
|
|
31
|
+
private_class_method :location_from
|
|
32
|
+
|
|
33
|
+
def initialize(severity:, kind:, requirement_id:, message:, citations: [],
|
|
34
|
+
remediation: nil, context: {}, location: nil,
|
|
35
|
+
extensions: {})
|
|
36
|
+
@severity = Validation.one_of(severity, SEVERITIES, "severity")
|
|
37
|
+
@kind = Validation.string(kind, "kind")
|
|
38
|
+
@requirement_id = Validation.string(requirement_id, "requirement_id")
|
|
39
|
+
@message = Validation.string(message, "message")
|
|
40
|
+
@citations = Citation.check_list(citations)
|
|
41
|
+
@remediation = Validation.optional_string(remediation, "remediation")
|
|
42
|
+
@context = Validation.context(context)
|
|
43
|
+
@location = check_location(location)
|
|
44
|
+
super(extensions: extensions)
|
|
45
|
+
freeze
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
SEVERITIES.each do |name|
|
|
49
|
+
define_method(:"#{name}?") { severity == name }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def check_location(location)
|
|
55
|
+
return location if location.nil? || location.is_a?(Location)
|
|
56
|
+
|
|
57
|
+
Validation.fail_at("must be a LensCore::Location", "location")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def standard_members
|
|
61
|
+
MEMBERS.to_h { |name| [name, public_send(name)] }.merge(
|
|
62
|
+
"citations" => citations.map(&:to_h), "location" => location&.to_h,
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LensCore
|
|
4
|
+
# Base class of every error LensCore raises for bad input.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# The text is not well-formed JSON or YAML (or is not a document at all).
|
|
8
|
+
class ParseError < Error; end
|
|
9
|
+
|
|
10
|
+
# The YAML uses a feature that is not safe to load: an object tag, a symbol,
|
|
11
|
+
# or an alias.
|
|
12
|
+
class UnsafeYamlError < Error; end
|
|
13
|
+
|
|
14
|
+
# The text, or a string inside it, is not valid UTF-8.
|
|
15
|
+
class InvalidEncodingError < Error; end
|
|
16
|
+
|
|
17
|
+
# The document declares a schema version other than LensCore::SCHEMA_VERSION.
|
|
18
|
+
class UnsupportedVersionError < Error; end
|
|
19
|
+
|
|
20
|
+
# The document is well-formed but breaks the schema. `path` names the
|
|
21
|
+
# offending member, e.g. "diagnostics[2].location.line".
|
|
22
|
+
class SchemaError < Error
|
|
23
|
+
attr_reader :detail, :path
|
|
24
|
+
|
|
25
|
+
def initialize(detail, path = [])
|
|
26
|
+
@detail = detail
|
|
27
|
+
@path = path.freeze
|
|
28
|
+
super(path.empty? ? detail : "#{format_path}: #{detail}")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The same error, located one level further out.
|
|
32
|
+
def within(segment)
|
|
33
|
+
self.class.new(detail, [segment, *path])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def format_path
|
|
39
|
+
path.each_with_index.map do |seg, i|
|
|
40
|
+
if seg.is_a?(Integer)
|
|
41
|
+
"[#{seg}]"
|
|
42
|
+
else
|
|
43
|
+
(i.zero? ? seg.to_s : ".#{seg}")
|
|
44
|
+
end
|
|
45
|
+
end.join
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LensCore
|
|
4
|
+
# Where a diagnostic applies. `type` says which coordinates are meaningful;
|
|
5
|
+
# a coordinate that is not known is nil and is omitted from output, never
|
|
6
|
+
# replaced by a placeholder.
|
|
7
|
+
#
|
|
8
|
+
# xml line, column (1-based), path
|
|
9
|
+
# binary byte_offset, byte_length, record_index (0-based), record_type
|
|
10
|
+
class Location < Node
|
|
11
|
+
TYPES = %w[xml binary].freeze
|
|
12
|
+
FIELDS = {
|
|
13
|
+
"xml" => %w[line column path],
|
|
14
|
+
"binary" => %w[byte_offset byte_length record_index record_type],
|
|
15
|
+
}.freeze
|
|
16
|
+
MEMBERS = (["type"] + FIELDS.values.flatten).freeze
|
|
17
|
+
MINIMUMS = {
|
|
18
|
+
"line" => 1, "column" => 1,
|
|
19
|
+
"byte_offset" => 0, "byte_length" => 0, "record_index" => 0
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
attr_reader :type, :coordinates
|
|
23
|
+
|
|
24
|
+
def self.from_h(hash)
|
|
25
|
+
known, extra = split(hash)
|
|
26
|
+
type = Validation.one_of(known["type"], TYPES, "type")
|
|
27
|
+
new(type: type, **known.except("type").transform_keys(&:to_sym),
|
|
28
|
+
extensions: extra)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.xml(line: nil, column: nil, path: nil, extensions: {})
|
|
32
|
+
new(type: "xml", line: line, column: column, path: path,
|
|
33
|
+
extensions: extensions)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.binary(byte_offset: nil, byte_length: nil, record_index: nil,
|
|
37
|
+
record_type: nil, extensions: {})
|
|
38
|
+
new(type: "binary", byte_offset: byte_offset, byte_length: byte_length,
|
|
39
|
+
record_index: record_index, record_type: record_type,
|
|
40
|
+
extensions: extensions)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize(type:, extensions: {}, **fields)
|
|
44
|
+
@type = Validation.one_of(type, TYPES, "type")
|
|
45
|
+
@coordinates = coordinates_from(fields)
|
|
46
|
+
super(extensions: extensions)
|
|
47
|
+
freeze
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
FIELDS.values.flatten.each do |name|
|
|
51
|
+
define_method(name) { coordinates[name] }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def coordinates_from(fields)
|
|
57
|
+
applicable = FIELDS.fetch(type)
|
|
58
|
+
reject_stray(fields, applicable)
|
|
59
|
+
applicable.to_h { |name| [name, check(name, fields[name.to_sym])] }
|
|
60
|
+
.compact.freeze
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def reject_stray(fields, applicable)
|
|
64
|
+
stray = fields.keys.map(&:to_s) - applicable
|
|
65
|
+
return if stray.empty?
|
|
66
|
+
|
|
67
|
+
Validation.fail_at("is not a #{type} coordinate", stray.first)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def check(name, value)
|
|
71
|
+
return nil if value.nil?
|
|
72
|
+
return Validation.string(value, name) unless MINIMUMS.key?(name)
|
|
73
|
+
|
|
74
|
+
Validation.integer(value, name, min: MINIMUMS.fetch(name))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def standard_members
|
|
78
|
+
{ "type" => type }.merge(coordinates)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|