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
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module LensCore
|
|
7
|
+
# Shared checks and canonicalization used by every value object, so a
|
|
8
|
+
# report built in Ruby and a report parsed from text obey the same rules.
|
|
9
|
+
# `name` is a member name or a path (Array) to one; errors carry it.
|
|
10
|
+
module Validation
|
|
11
|
+
ABSOLUTE_URI = /\A[A-Za-z][A-Za-z0-9+.-]*:\S+\z/
|
|
12
|
+
TIMESTAMP = /\A(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})T
|
|
13
|
+
(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?:\.\d+)?
|
|
14
|
+
(?:Z|[+-](?<off_hour>\d{2}):(?<off_minute>\d{2}))\z/x
|
|
15
|
+
CLOCK_LIMITS = { "hour" => 23, "minute" => 59, "second" => 59,
|
|
16
|
+
"off_hour" => 23, "off_minute" => 59 }.freeze
|
|
17
|
+
TEXT_ENCODINGS = [Encoding::UTF_8, Encoding::US_ASCII].freeze
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def fail_at(detail, name)
|
|
22
|
+
raise SchemaError.new(detail, Array(name))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def required(value, name)
|
|
26
|
+
fail_at("is required", name) if value.nil?
|
|
27
|
+
value
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def object(value, name = nil)
|
|
31
|
+
return value if required(value, name).is_a?(Hash)
|
|
32
|
+
|
|
33
|
+
fail_at("must be an object", name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def array(value, name)
|
|
37
|
+
return value if required(value, name).is_a?(Array)
|
|
38
|
+
|
|
39
|
+
fail_at("must be an array", name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def string(value, name, allow_empty: false)
|
|
43
|
+
required(value, name)
|
|
44
|
+
fail_at("must be a string", name) unless value.is_a?(String)
|
|
45
|
+
|
|
46
|
+
text(value, name)
|
|
47
|
+
fail_at("must not be empty", name) if value.empty? && !allow_empty
|
|
48
|
+
|
|
49
|
+
value.frozen? ? value : value.dup.freeze
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# An optional string member: nil stays nil.
|
|
53
|
+
def optional_string(value, name)
|
|
54
|
+
value.nil? ? nil : string(value, name)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def integer(value, name, min:)
|
|
58
|
+
required(value, name)
|
|
59
|
+
fail_at("must be an integer", name) unless value.is_a?(Integer)
|
|
60
|
+
fail_at("must be >= #{min}", name) if value < min
|
|
61
|
+
|
|
62
|
+
value
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def boolean(value, name)
|
|
66
|
+
return value if [true, false].include?(required(value, name))
|
|
67
|
+
|
|
68
|
+
fail_at("must be true or false", name)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def one_of(value, allowed, name)
|
|
72
|
+
value = value.to_s if value.is_a?(Symbol)
|
|
73
|
+
return value if allowed.include?(string(value, name))
|
|
74
|
+
|
|
75
|
+
fail_at("must be one of #{allowed.join(', ')}", name)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def absolute_uri(value, name)
|
|
79
|
+
return value if ABSOLUTE_URI.match?(string(value, name))
|
|
80
|
+
|
|
81
|
+
fail_at("must be an absolute URI", name)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Accepts a Time or an RFC 3339 string with an explicit offset; returns a
|
|
85
|
+
# frozen UTC Time, or nil when the member is absent.
|
|
86
|
+
def timestamp(value, name)
|
|
87
|
+
return nil if value.nil?
|
|
88
|
+
return value.dup.utc.freeze if value.is_a?(Time)
|
|
89
|
+
|
|
90
|
+
string(value, name)
|
|
91
|
+
return Time.iso8601(value).utc.freeze if rfc3339?(value)
|
|
92
|
+
|
|
93
|
+
fail_at("must be an RFC 3339 timestamp", name)
|
|
94
|
+
rescue ArgumentError
|
|
95
|
+
fail_at("must be an RFC 3339 timestamp", name)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The lexical form plus real calendar and clock values, so Time never
|
|
99
|
+
# rolls an impossible date or hour 24 over into the next day.
|
|
100
|
+
def rfc3339?(value)
|
|
101
|
+
match = TIMESTAMP.match(value) or return false
|
|
102
|
+
part = match.named_captures.transform_values(&:to_i)
|
|
103
|
+
|
|
104
|
+
Date.valid_date?(part["year"], part["month"], part["day"]) &&
|
|
105
|
+
CLOCK_LIMITS.all? { |key, max| part[key] <= max }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def text(value, name)
|
|
109
|
+
return if TEXT_ENCODINGS.include?(value.encoding) && value.valid_encoding?
|
|
110
|
+
|
|
111
|
+
raise InvalidEncodingError, "#{Array(name).join('.')}: not valid UTF-8"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Deep copy of a JSON-safe value, frozen, with object keys sorted
|
|
115
|
+
# lexically (by bytes). Raises SchemaError for anything JSON cannot carry.
|
|
116
|
+
def json_value(value, name)
|
|
117
|
+
case value
|
|
118
|
+
when nil, true, false, Integer, Float, String
|
|
119
|
+
json_scalar(value, name)
|
|
120
|
+
when Array then json_array(value, name)
|
|
121
|
+
when Hash then json_object(value, name)
|
|
122
|
+
else fail_at("must be JSON-safe (got #{value.class})", name)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def json_scalar(value, name)
|
|
127
|
+
if value.is_a?(Float) && !value.finite?
|
|
128
|
+
fail_at("must be a finite number", name)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
value.is_a?(String) ? string(value, name, allow_empty: true) : value
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def json_array(list, name)
|
|
135
|
+
list.each_with_index.map { |v, i| json_value(v, [*name, i]) }.freeze
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def json_object(hash, name)
|
|
139
|
+
hash.each_key do |key|
|
|
140
|
+
fail_at("object keys must be strings", name) unless key.is_a?(String)
|
|
141
|
+
|
|
142
|
+
text(key, name)
|
|
143
|
+
end
|
|
144
|
+
hash.sort_by { |key, _| key.b }
|
|
145
|
+
.to_h { |key, v| [key.dup.freeze, json_value(v, [*name, key])] }
|
|
146
|
+
.freeze
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# A diagnostic's `context`: a JSON-safe object.
|
|
150
|
+
def context(value)
|
|
151
|
+
json_object(object(value, "context"), "context")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Extension members: JSON-safe values under keys that are not standard.
|
|
155
|
+
def extensions(hash, standard, name = [])
|
|
156
|
+
json_object(object(hash, name), name).tap do |ext|
|
|
157
|
+
clash = ext.keys & standard
|
|
158
|
+
next if clash.empty?
|
|
159
|
+
|
|
160
|
+
fail_at("#{clash.first} is a standard member, not an extension", name)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LensCore
|
|
4
|
+
# Gem version.
|
|
5
|
+
VERSION = "0.1.0"
|
|
6
|
+
|
|
7
|
+
# Major version of the wire format this gem reads and writes. It is the
|
|
8
|
+
# value of the `schema_version` member and is independent of VERSION.
|
|
9
|
+
SCHEMA_VERSION = 1
|
|
10
|
+
end
|
data/lib/lens_core.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lens_core/version"
|
|
4
|
+
require_relative "lens_core/errors"
|
|
5
|
+
require_relative "lens_core/schema"
|
|
6
|
+
require_relative "lens_core/validation"
|
|
7
|
+
require_relative "lens_core/node"
|
|
8
|
+
require_relative "lens_core/citation"
|
|
9
|
+
require_relative "lens_core/location"
|
|
10
|
+
require_relative "lens_core/producer"
|
|
11
|
+
require_relative "lens_core/profile"
|
|
12
|
+
require_relative "lens_core/diagnostic"
|
|
13
|
+
require_relative "lens_core/report"
|
|
14
|
+
require_relative "lens_core/parsers/text"
|
|
15
|
+
require_relative "lens_core/parsers/json"
|
|
16
|
+
require_relative "lens_core/parsers/yaml"
|
|
17
|
+
require_relative "lens_core/renderers/json"
|
|
18
|
+
require_relative "lens_core/renderers/yaml"
|
|
19
|
+
require_relative "lens_core/renderers/human"
|
|
20
|
+
|
|
21
|
+
# Format-neutral conformance diagnostics: a report model (schema version
|
|
22
|
+
# LensCore::SCHEMA_VERSION), JSON and YAML parsers, and JSON, YAML and
|
|
23
|
+
# human-readable renderers. It has no runtime dependencies.
|
|
24
|
+
module LensCore
|
|
25
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/claricle/lens_core/main/schemas/lens-diagnostics-v1.json",
|
|
4
|
+
"title": "LensCore diagnostics report, schema version 1",
|
|
5
|
+
"description": "Normative wire format. Unknown object members are permitted everywhere (additive evolution) except inside `summary`, where they are ignored. See docs/diagnostics-v1.adoc for the rules JSON Schema cannot express.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["schema_version", "producer", "profile", "valid", "summary", "diagnostics"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schema_version": { "const": 1 },
|
|
10
|
+
"producer": { "$ref": "#/$defs/producer" },
|
|
11
|
+
"profile": { "$ref": "#/$defs/profile" },
|
|
12
|
+
"timestamp": { "$ref": "#/$defs/timestamp" },
|
|
13
|
+
"valid": { "type": "boolean" },
|
|
14
|
+
"summary": { "$ref": "#/$defs/summary" },
|
|
15
|
+
"diagnostics": {
|
|
16
|
+
"type": "array",
|
|
17
|
+
"items": { "$ref": "#/$defs/diagnostic" }
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"if": { "properties": { "valid": { "const": true } }, "required": ["valid"] },
|
|
21
|
+
"then": { "properties": { "summary": { "properties": { "errors": { "const": 0 } } } } },
|
|
22
|
+
"$defs": {
|
|
23
|
+
"text": { "type": "string", "minLength": 1 },
|
|
24
|
+
"count": { "type": "integer", "minimum": 0 },
|
|
25
|
+
"timestamp": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"format": "date-time",
|
|
28
|
+
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})$"
|
|
29
|
+
},
|
|
30
|
+
"producer": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"required": ["name", "version"],
|
|
33
|
+
"properties": {
|
|
34
|
+
"name": { "$ref": "#/$defs/text" },
|
|
35
|
+
"version": { "$ref": "#/$defs/text" }
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"profile": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"required": ["id", "citations"],
|
|
41
|
+
"properties": {
|
|
42
|
+
"id": { "$ref": "#/$defs/text" },
|
|
43
|
+
"citations": { "$ref": "#/$defs/citations" }
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"summary": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"required": ["errors", "warnings", "infos"],
|
|
49
|
+
"properties": {
|
|
50
|
+
"errors": { "$ref": "#/$defs/count" },
|
|
51
|
+
"warnings": { "$ref": "#/$defs/count" },
|
|
52
|
+
"infos": { "$ref": "#/$defs/count" }
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"citations": {
|
|
56
|
+
"type": "array",
|
|
57
|
+
"items": { "$ref": "#/$defs/citation" }
|
|
58
|
+
},
|
|
59
|
+
"citation": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"required": ["uri"],
|
|
62
|
+
"properties": {
|
|
63
|
+
"uri": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9+.-]*:\\S+$" },
|
|
64
|
+
"label": { "$ref": "#/$defs/text" }
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"diagnostic": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"required": ["severity", "kind", "requirement_id", "message"],
|
|
70
|
+
"properties": {
|
|
71
|
+
"severity": { "enum": ["error", "warning", "info"] },
|
|
72
|
+
"kind": { "$ref": "#/$defs/text" },
|
|
73
|
+
"requirement_id": { "$ref": "#/$defs/text" },
|
|
74
|
+
"message": { "$ref": "#/$defs/text" },
|
|
75
|
+
"citations": { "$ref": "#/$defs/citations" },
|
|
76
|
+
"remediation": { "$ref": "#/$defs/text" },
|
|
77
|
+
"context": { "type": "object" },
|
|
78
|
+
"location": { "$ref": "#/$defs/location" }
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"location": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"required": ["type"],
|
|
84
|
+
"properties": {
|
|
85
|
+
"type": { "enum": ["xml", "binary"] }
|
|
86
|
+
},
|
|
87
|
+
"allOf": [
|
|
88
|
+
{
|
|
89
|
+
"if": { "properties": { "type": { "const": "xml" } }, "required": ["type"] },
|
|
90
|
+
"then": {
|
|
91
|
+
"properties": {
|
|
92
|
+
"line": { "type": "integer", "minimum": 1 },
|
|
93
|
+
"column": { "type": "integer", "minimum": 1 },
|
|
94
|
+
"path": { "$ref": "#/$defs/text" },
|
|
95
|
+
"byte_offset": false,
|
|
96
|
+
"byte_length": false,
|
|
97
|
+
"record_index": false,
|
|
98
|
+
"record_type": false
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"if": { "properties": { "type": { "const": "binary" } }, "required": ["type"] },
|
|
104
|
+
"then": {
|
|
105
|
+
"properties": {
|
|
106
|
+
"byte_offset": { "type": "integer", "minimum": 0 },
|
|
107
|
+
"byte_length": { "type": "integer", "minimum": 0 },
|
|
108
|
+
"record_index": { "type": "integer", "minimum": 0 },
|
|
109
|
+
"record_type": { "$ref": "#/$defs/text" },
|
|
110
|
+
"line": false,
|
|
111
|
+
"column": false,
|
|
112
|
+
"path": false
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
metadata
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lens_core
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Ribose
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: One diagnostics report shape (schema version 1) that any format checker
|
|
13
|
+
can emit, with JSON and YAML parsers and JSON, YAML and human-readable renderers.
|
|
14
|
+
No runtime dependencies.
|
|
15
|
+
email:
|
|
16
|
+
- open.source@ribose.com
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- Gemfile
|
|
22
|
+
- LICENSE
|
|
23
|
+
- README.adoc
|
|
24
|
+
- Rakefile
|
|
25
|
+
- docs/diagnostics-v1.adoc
|
|
26
|
+
- lens_core.gemspec
|
|
27
|
+
- lib/lens_core.rb
|
|
28
|
+
- lib/lens_core/citation.rb
|
|
29
|
+
- lib/lens_core/diagnostic.rb
|
|
30
|
+
- lib/lens_core/errors.rb
|
|
31
|
+
- lib/lens_core/location.rb
|
|
32
|
+
- lib/lens_core/node.rb
|
|
33
|
+
- lib/lens_core/parsers/json.rb
|
|
34
|
+
- lib/lens_core/parsers/text.rb
|
|
35
|
+
- lib/lens_core/parsers/yaml.rb
|
|
36
|
+
- lib/lens_core/producer.rb
|
|
37
|
+
- lib/lens_core/profile.rb
|
|
38
|
+
- lib/lens_core/renderers/human.rb
|
|
39
|
+
- lib/lens_core/renderers/json.rb
|
|
40
|
+
- lib/lens_core/renderers/yaml.rb
|
|
41
|
+
- lib/lens_core/report.rb
|
|
42
|
+
- lib/lens_core/schema.rb
|
|
43
|
+
- lib/lens_core/validation.rb
|
|
44
|
+
- lib/lens_core/version.rb
|
|
45
|
+
- schemas/lens-diagnostics-v1.json
|
|
46
|
+
homepage: https://github.com/claricle/lens_core
|
|
47
|
+
licenses:
|
|
48
|
+
- BSD-2-Clause
|
|
49
|
+
metadata:
|
|
50
|
+
homepage_uri: https://github.com/claricle/lens_core
|
|
51
|
+
source_code_uri: https://github.com/claricle/lens_core
|
|
52
|
+
bug_tracker_uri: https://github.com/claricle/lens_core/issues
|
|
53
|
+
rubygems_mfa_required: 'true'
|
|
54
|
+
rdoc_options: []
|
|
55
|
+
require_paths:
|
|
56
|
+
- lib
|
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: 3.1.0
|
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '0'
|
|
67
|
+
requirements: []
|
|
68
|
+
rubygems_version: 3.6.9
|
|
69
|
+
specification_version: 4
|
|
70
|
+
summary: Format-neutral conformance diagnostics model and renderers.
|
|
71
|
+
test_files: []
|