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.
@@ -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: []