elasticgraph-support 1.0.0.rc4 → 1.0.1
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/README.md +8 -4
- data/lib/elastic_graph/support/config.rb +185 -0
- data/lib/elastic_graph/support/json_schema/json_schema_draft_7_schema.json +172 -0
- data/lib/elastic_graph/support/json_schema/meta_schema_validator.rb +71 -0
- data/lib/elastic_graph/support/json_schema/validator.rb +91 -0
- data/lib/elastic_graph/support/json_schema/validator_factory.rb +116 -0
- data/lib/elastic_graph/support/logger.rb +53 -42
- data/lib/elastic_graph/version.rb +1 -1
- metadata +23 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 389ea95870f3da886ff56a807f488dd15bc9a7fb530e739a8f832392db2cf898
|
4
|
+
data.tar.gz: 39e463b98ef99ec346895631686af930e0a249232b2fba12c93763d75983215a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1e1ec0359179fb3f33e58c6f4d0e18d3009d46e3fe13bcf2da07a2a3aa44b3252a1c560d2bb9d44e2bde7a0da1af81b546f7ac974c7976dc683bf9ae21464e28
|
7
|
+
data.tar.gz: 1f47d4e0805111898bbc8e4c62b0e46edd040e07fed8b54088d8ffaf2f01ef4552540836e56b8b68e60e47a762a65a307c3ac4cac9fbba1d6ce94b86b26d7894
|
data/README.md
CHANGED
@@ -3,8 +3,11 @@
|
|
3
3
|
This gem provides support utilities for the rest of the ElasticGraph gems. As
|
4
4
|
such, it is not intended to provide any public APIs for ElasticGraph users.
|
5
5
|
|
6
|
+
It includes JSON Schema validation functionality and other common utilities.
|
7
|
+
|
6
8
|
Importantly, it is intended to have as few dependencies as possible: it currently
|
7
|
-
only depends on `logger` (which originated in the Ruby standard library)
|
9
|
+
only depends on `logger` (which originated in the Ruby standard library) and
|
10
|
+
`json_schemer` for JSON Schema validation.
|
8
11
|
|
9
12
|
## Dependency Diagram
|
10
13
|
|
@@ -18,6 +21,9 @@ graph LR;
|
|
18
21
|
logger["logger"];
|
19
22
|
elasticgraph-support --> logger;
|
20
23
|
class logger externalGemStyle;
|
24
|
+
json_schemer["json_schemer"];
|
25
|
+
elasticgraph-support --> json_schemer;
|
26
|
+
class json_schemer externalGemStyle;
|
21
27
|
elasticgraph["elasticgraph"];
|
22
28
|
elasticgraph --> elasticgraph-support;
|
23
29
|
class elasticgraph otherEgGemStyle;
|
@@ -39,9 +45,6 @@ graph LR;
|
|
39
45
|
elasticgraph-indexer["elasticgraph-indexer"];
|
40
46
|
elasticgraph-indexer --> elasticgraph-support;
|
41
47
|
class elasticgraph-indexer otherEgGemStyle;
|
42
|
-
elasticgraph-json_schema["elasticgraph-json_schema"];
|
43
|
-
elasticgraph-json_schema --> elasticgraph-support;
|
44
|
-
class elasticgraph-json_schema otherEgGemStyle;
|
45
48
|
elasticgraph-opensearch["elasticgraph-opensearch"];
|
46
49
|
elasticgraph-opensearch --> elasticgraph-support;
|
47
50
|
class elasticgraph-opensearch otherEgGemStyle;
|
@@ -55,4 +58,5 @@ graph LR;
|
|
55
58
|
elasticgraph-schema_definition --> elasticgraph-support;
|
56
59
|
class elasticgraph-schema_definition otherEgGemStyle;
|
57
60
|
click logger href "https://rubygems.org/gems/logger" "Open on RubyGems.org" _blank;
|
61
|
+
click json_schemer href "https://rubygems.org/gems/json_schemer" "Open on RubyGems.org" _blank;
|
58
62
|
```
|
@@ -0,0 +1,185 @@
|
|
1
|
+
# Copyright 2024 - 2025 Block, Inc.
|
2
|
+
#
|
3
|
+
# Use of this source code is governed by an MIT-style
|
4
|
+
# license that can be found in the LICENSE file or at
|
5
|
+
# https://opensource.org/licenses/MIT.
|
6
|
+
#
|
7
|
+
# frozen_string_literal: true
|
8
|
+
|
9
|
+
require "elastic_graph/errors"
|
10
|
+
require "elastic_graph/support/json_schema/validator_factory"
|
11
|
+
require "elastic_graph/support/from_yaml_file"
|
12
|
+
require "elastic_graph/support/hash_util"
|
13
|
+
require "json_schemer"
|
14
|
+
|
15
|
+
module ElasticGraph
|
16
|
+
module Support
|
17
|
+
# Provides a standard way to define an ElasticGraph configuration class.
|
18
|
+
module Config
|
19
|
+
# Defines a configuration class with the given attributes.
|
20
|
+
#
|
21
|
+
# @param attrs [::Symbol] attribute names
|
22
|
+
# @return [::Class] the defined configuration class
|
23
|
+
# @yield [::Data] the body of the class (similar to `::Data.define`)
|
24
|
+
#
|
25
|
+
# @example Define a configuration class
|
26
|
+
# require "elastic_graph/support/config"
|
27
|
+
#
|
28
|
+
# ExampleConfigClass = ElasticGraph::Support::Config.define(:size, :name) do
|
29
|
+
# json_schema at: "example", optional: false,
|
30
|
+
# properties: {
|
31
|
+
# size: {
|
32
|
+
# description: "Determines the size.",
|
33
|
+
# examples: [10, 100],
|
34
|
+
# type: "integer",
|
35
|
+
# minimum: 1,
|
36
|
+
# },
|
37
|
+
# name: {
|
38
|
+
# description: "Determines the name.",
|
39
|
+
# examples: ["widget"],
|
40
|
+
# type: "string",
|
41
|
+
# minLength: 1
|
42
|
+
# }
|
43
|
+
# },
|
44
|
+
# required: ["size", "name"]
|
45
|
+
# end
|
46
|
+
def self.define(*attrs, &block)
|
47
|
+
::Data.define(*attrs) do
|
48
|
+
# @implements ::Data
|
49
|
+
alias_method :__data_initialize, :initialize
|
50
|
+
extend ClassMethods
|
51
|
+
include InstanceMethods
|
52
|
+
class_exec(&(_ = block)) if block
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Defines class methods for configuration classes.
|
57
|
+
module ClassMethods
|
58
|
+
include Support::FromYamlFile
|
59
|
+
|
60
|
+
# @dynamic validator, path, required
|
61
|
+
|
62
|
+
# @return [Support::JSONSchema::Validator] validator for this configuration class
|
63
|
+
attr_reader :validator
|
64
|
+
|
65
|
+
# @return [::String] path from the global configuration root to where this configuration resides.
|
66
|
+
attr_reader :path
|
67
|
+
|
68
|
+
# @return [::Boolean] whether this configuration property is required
|
69
|
+
attr_reader :required
|
70
|
+
|
71
|
+
# Defines the JSON schema and path for this configuration class.
|
72
|
+
#
|
73
|
+
# @param at [::String] path from the global configuration root to where this configuration resides
|
74
|
+
# @param optional [::Boolean] whether configuration at the provided `path` is optional
|
75
|
+
# @param schema [::Hash<::Symbol, ::Object>] JSON schema definition
|
76
|
+
# @return [void]
|
77
|
+
#
|
78
|
+
# @example Define a configuration class
|
79
|
+
# require "elastic_graph/support/config"
|
80
|
+
#
|
81
|
+
# ExampleConfigClass = ElasticGraph::Support::Config.define(:size, :name) do
|
82
|
+
# json_schema at: "example", optional: false,
|
83
|
+
# properties: {
|
84
|
+
# size: {
|
85
|
+
# description: "Determines the size.",
|
86
|
+
# examples: [10, 100],
|
87
|
+
# type: "integer",
|
88
|
+
# minimum: 1,
|
89
|
+
# },
|
90
|
+
# name: {
|
91
|
+
# description: "Determines the name.",
|
92
|
+
# examples: ["widget"],
|
93
|
+
# type: "string",
|
94
|
+
# minLength: 1
|
95
|
+
# }
|
96
|
+
# },
|
97
|
+
# required: ["size", "name"]
|
98
|
+
# end
|
99
|
+
def json_schema(at:, optional:, **schema)
|
100
|
+
@path = at
|
101
|
+
@required = !optional
|
102
|
+
|
103
|
+
schema = Support::HashUtil.stringify_keys(schema)
|
104
|
+
@validator = Support::JSONSchema::ValidatorFactory
|
105
|
+
.new(schema: {"$schema" => "http://json-schema.org/draft-07/schema#", "$defs" => {"config" => schema}}, sanitize_pii: false)
|
106
|
+
.with_unknown_properties_disallowed
|
107
|
+
.validator_for("config")
|
108
|
+
end
|
109
|
+
|
110
|
+
# Instantiates a config instance from the given parsed YAML class, returning `nil` if there is no config.
|
111
|
+
# In addition, this (along with `Support::FromYamlFile`) makes `from_yaml_file(path_to_file)` available.
|
112
|
+
#
|
113
|
+
# @param parsed_yaml [::Hash<::String, ::Object>] config hash parsed from YAML
|
114
|
+
# @return [::Data, nil] the instantiated config object or `nil` if there is nothing at the specified path
|
115
|
+
def from_parsed_yaml(parsed_yaml)
|
116
|
+
value_at_path = Support::HashUtil.fetch_value_at_path(parsed_yaml, path.split(".")) { return nil }
|
117
|
+
|
118
|
+
if value_at_path.is_a?(::Hash)
|
119
|
+
config = (_ = value_at_path).transform_keys(&:to_sym) # : ::Hash[::Symbol, untyped]
|
120
|
+
new(**config)
|
121
|
+
else
|
122
|
+
raise_invalid_config("Expected a hash at `#{path}`, got: `#{value_at_path.inspect}`.")
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Instantiates a config instance from the given parsed YAML class, raising an error if there is no config.
|
127
|
+
#
|
128
|
+
# @param parsed_yaml [::Hash<::String, ::Object>] config hash parsed from YAML
|
129
|
+
# @return [::Data] the instantiated config object
|
130
|
+
# @raise [Errors::ConfigError] if there is no config at the specified path.
|
131
|
+
def from_parsed_yaml!(parsed_yaml)
|
132
|
+
from_parsed_yaml(parsed_yaml) || raise_invalid_config("missing configuration at `#{path}`.")
|
133
|
+
end
|
134
|
+
|
135
|
+
# @private
|
136
|
+
def raise_invalid_config(error)
|
137
|
+
raise Errors::ConfigError, "Invalid configuration for `#{name}` at `#{path}`: #{error}"
|
138
|
+
end
|
139
|
+
|
140
|
+
# Like `new`, but avoids applying JSON schema validation. This is needed so that we can make
|
141
|
+
# `#with` work correctly with the validation and conversion features we offer.
|
142
|
+
#
|
143
|
+
# @private
|
144
|
+
def new_without_validation(**data)
|
145
|
+
instance = allocate
|
146
|
+
instance.send(:__data_initialize, **data)
|
147
|
+
instance
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# @private
|
152
|
+
module InstanceMethods
|
153
|
+
# Overrides `initialize` to apply JSON schema validation.
|
154
|
+
def initialize(**config)
|
155
|
+
klass = (_ = self.class) # : ClassMethods[::Data]
|
156
|
+
validator = klass.validator
|
157
|
+
config = validator.merge_defaults(config)
|
158
|
+
|
159
|
+
if (error = validator.validate_with_error_message(config))
|
160
|
+
klass.raise_invalid_config(error)
|
161
|
+
end
|
162
|
+
|
163
|
+
config = config.transform_keys(&:to_sym)
|
164
|
+
__skip__ = super(**convert_values(**config))
|
165
|
+
end
|
166
|
+
|
167
|
+
# Overrides `#with` to bypass the normal JSON schema validation that applies in `#initialize`.
|
168
|
+
# This is required so that `config.with(...)` can be used on config classes that use the
|
169
|
+
# `convert_values` hook to convert JSON data to some custom Ruby type. The custom Ruby type
|
170
|
+
# won't pass JSON schema validation, and if we didn't override `with` then we'd get validation
|
171
|
+
# failures due to the converted values failing validation.
|
172
|
+
def with(**updates)
|
173
|
+
(_ = self.class).new_without_validation(**to_h.merge(updates))
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
|
178
|
+
# Default implementation of a hook that allows config values to be converted during initialization.
|
179
|
+
def convert_values(**values)
|
180
|
+
values
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
{
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
3
|
+
"$id": "http://json-schema.org/draft-07/schema#",
|
4
|
+
"title": "Core schema meta-schema",
|
5
|
+
"definitions": {
|
6
|
+
"schemaArray": {
|
7
|
+
"type": "array",
|
8
|
+
"minItems": 1,
|
9
|
+
"items": { "$ref": "#" }
|
10
|
+
},
|
11
|
+
"nonNegativeInteger": {
|
12
|
+
"type": "integer",
|
13
|
+
"minimum": 0
|
14
|
+
},
|
15
|
+
"nonNegativeIntegerDefault0": {
|
16
|
+
"allOf": [
|
17
|
+
{ "$ref": "#/definitions/nonNegativeInteger" },
|
18
|
+
{ "default": 0 }
|
19
|
+
]
|
20
|
+
},
|
21
|
+
"simpleTypes": {
|
22
|
+
"enum": [
|
23
|
+
"array",
|
24
|
+
"boolean",
|
25
|
+
"integer",
|
26
|
+
"null",
|
27
|
+
"number",
|
28
|
+
"object",
|
29
|
+
"string"
|
30
|
+
]
|
31
|
+
},
|
32
|
+
"stringArray": {
|
33
|
+
"type": "array",
|
34
|
+
"items": { "type": "string" },
|
35
|
+
"uniqueItems": true,
|
36
|
+
"default": []
|
37
|
+
}
|
38
|
+
},
|
39
|
+
"type": ["object", "boolean"],
|
40
|
+
"properties": {
|
41
|
+
"$id": {
|
42
|
+
"type": "string",
|
43
|
+
"format": "uri-reference"
|
44
|
+
},
|
45
|
+
"$schema": {
|
46
|
+
"type": "string",
|
47
|
+
"format": "uri"
|
48
|
+
},
|
49
|
+
"$ref": {
|
50
|
+
"type": "string",
|
51
|
+
"format": "uri-reference"
|
52
|
+
},
|
53
|
+
"$comment": {
|
54
|
+
"type": "string"
|
55
|
+
},
|
56
|
+
"title": {
|
57
|
+
"type": "string"
|
58
|
+
},
|
59
|
+
"description": {
|
60
|
+
"type": "string"
|
61
|
+
},
|
62
|
+
"default": true,
|
63
|
+
"readOnly": {
|
64
|
+
"type": "boolean",
|
65
|
+
"default": false
|
66
|
+
},
|
67
|
+
"writeOnly": {
|
68
|
+
"type": "boolean",
|
69
|
+
"default": false
|
70
|
+
},
|
71
|
+
"examples": {
|
72
|
+
"type": "array",
|
73
|
+
"items": true
|
74
|
+
},
|
75
|
+
"multipleOf": {
|
76
|
+
"type": "number",
|
77
|
+
"exclusiveMinimum": 0
|
78
|
+
},
|
79
|
+
"maximum": {
|
80
|
+
"type": "number"
|
81
|
+
},
|
82
|
+
"exclusiveMaximum": {
|
83
|
+
"type": "number"
|
84
|
+
},
|
85
|
+
"minimum": {
|
86
|
+
"type": "number"
|
87
|
+
},
|
88
|
+
"exclusiveMinimum": {
|
89
|
+
"type": "number"
|
90
|
+
},
|
91
|
+
"maxLength": { "$ref": "#/definitions/nonNegativeInteger" },
|
92
|
+
"minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
|
93
|
+
"pattern": {
|
94
|
+
"type": "string",
|
95
|
+
"format": "regex"
|
96
|
+
},
|
97
|
+
"additionalItems": { "$ref": "#" },
|
98
|
+
"items": {
|
99
|
+
"anyOf": [
|
100
|
+
{ "$ref": "#" },
|
101
|
+
{ "$ref": "#/definitions/schemaArray" }
|
102
|
+
],
|
103
|
+
"default": true
|
104
|
+
},
|
105
|
+
"maxItems": { "$ref": "#/definitions/nonNegativeInteger" },
|
106
|
+
"minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
|
107
|
+
"uniqueItems": {
|
108
|
+
"type": "boolean",
|
109
|
+
"default": false
|
110
|
+
},
|
111
|
+
"contains": { "$ref": "#" },
|
112
|
+
"maxProperties": { "$ref": "#/definitions/nonNegativeInteger" },
|
113
|
+
"minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
|
114
|
+
"required": { "$ref": "#/definitions/stringArray" },
|
115
|
+
"additionalProperties": { "$ref": "#" },
|
116
|
+
"definitions": {
|
117
|
+
"type": "object",
|
118
|
+
"additionalProperties": { "$ref": "#" },
|
119
|
+
"default": {}
|
120
|
+
},
|
121
|
+
"properties": {
|
122
|
+
"type": "object",
|
123
|
+
"additionalProperties": { "$ref": "#" },
|
124
|
+
"default": {}
|
125
|
+
},
|
126
|
+
"patternProperties": {
|
127
|
+
"type": "object",
|
128
|
+
"additionalProperties": { "$ref": "#" },
|
129
|
+
"propertyNames": { "format": "regex" },
|
130
|
+
"default": {}
|
131
|
+
},
|
132
|
+
"dependencies": {
|
133
|
+
"type": "object",
|
134
|
+
"additionalProperties": {
|
135
|
+
"anyOf": [
|
136
|
+
{ "$ref": "#" },
|
137
|
+
{ "$ref": "#/definitions/stringArray" }
|
138
|
+
]
|
139
|
+
}
|
140
|
+
},
|
141
|
+
"propertyNames": { "$ref": "#" },
|
142
|
+
"const": true,
|
143
|
+
"enum": {
|
144
|
+
"type": "array",
|
145
|
+
"items": true,
|
146
|
+
"minItems": 1,
|
147
|
+
"uniqueItems": true
|
148
|
+
},
|
149
|
+
"type": {
|
150
|
+
"anyOf": [
|
151
|
+
{ "$ref": "#/definitions/simpleTypes" },
|
152
|
+
{
|
153
|
+
"type": "array",
|
154
|
+
"items": { "$ref": "#/definitions/simpleTypes" },
|
155
|
+
"minItems": 1,
|
156
|
+
"uniqueItems": true
|
157
|
+
}
|
158
|
+
]
|
159
|
+
},
|
160
|
+
"format": { "type": "string" },
|
161
|
+
"contentMediaType": { "type": "string" },
|
162
|
+
"contentEncoding": { "type": "string" },
|
163
|
+
"if": { "$ref": "#" },
|
164
|
+
"then": { "$ref": "#" },
|
165
|
+
"else": { "$ref": "#" },
|
166
|
+
"allOf": { "$ref": "#/definitions/schemaArray" },
|
167
|
+
"anyOf": { "$ref": "#/definitions/schemaArray" },
|
168
|
+
"oneOf": { "$ref": "#/definitions/schemaArray" },
|
169
|
+
"not": { "$ref": "#" }
|
170
|
+
},
|
171
|
+
"default": true
|
172
|
+
}
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# Copyright 2024 - 2025 Block, Inc.
|
2
|
+
#
|
3
|
+
# Use of this source code is governed by an MIT-style
|
4
|
+
# license that can be found in the LICENSE file or at
|
5
|
+
# https://opensource.org/licenses/MIT.
|
6
|
+
#
|
7
|
+
# frozen_string_literal: true
|
8
|
+
|
9
|
+
require "elastic_graph/support/json_schema/validator"
|
10
|
+
require "elastic_graph/support/json_schema/validator_factory"
|
11
|
+
require "elastic_graph/support/hash_util"
|
12
|
+
require "json"
|
13
|
+
|
14
|
+
module ElasticGraph
|
15
|
+
module Support
|
16
|
+
# Provides [JSON Schema](https://json-schema.org/) validation for ElasticGraph.
|
17
|
+
module JSONSchema
|
18
|
+
# Provides a validator to validate a JSON schema definitions according to the JSON schema meta schema.
|
19
|
+
# The validator is configured to validate strictly, so that non-standard JSON schema properties are disallowed.
|
20
|
+
#
|
21
|
+
# @return [Validator]
|
22
|
+
# @see .elastic_graph_internal_meta_schema_validator
|
23
|
+
def self.strict_meta_schema_validator
|
24
|
+
@strict_meta_schema_validator ||= MetaSchemaLoader.load_strict_validator
|
25
|
+
end
|
26
|
+
|
27
|
+
# Provides a validator to validate a JSON schema definition according to the JSON schema meta schema.
|
28
|
+
# The validator is configured to validate strictly, so that non-standard JSON schema properties are disallowed,
|
29
|
+
# except for internal ElasticGraph metadata properties.
|
30
|
+
#
|
31
|
+
# @return [Validator]
|
32
|
+
# @see .strict_meta_schema_validator
|
33
|
+
def self.elastic_graph_internal_meta_schema_validator
|
34
|
+
@elastic_graph_internal_meta_schema_validator ||= MetaSchemaLoader.load_strict_validator({
|
35
|
+
"properties" => {
|
36
|
+
"ElasticGraph" => {
|
37
|
+
"type" => "object",
|
38
|
+
"required" => ["type", "nameInIndex"],
|
39
|
+
"properties" => {
|
40
|
+
"type" => {"type" => "string"},
|
41
|
+
"nameInIndex" => {"type" => "string"}
|
42
|
+
}
|
43
|
+
}
|
44
|
+
}
|
45
|
+
})
|
46
|
+
end
|
47
|
+
|
48
|
+
# Responsible for building {Validator}s that can validate JSON schema definitions.
|
49
|
+
module MetaSchemaLoader
|
50
|
+
# Builds a validator to validate a JSON schema definition according to the JSON schema meta schema.
|
51
|
+
#
|
52
|
+
# @param overrides [Hash<String, Object>] meta schema overrides
|
53
|
+
def self.load_strict_validator(overrides = {})
|
54
|
+
# Downloaded from: https://json-schema.org/draft-07/schema
|
55
|
+
schema = ::JSON.parse(::File.read(::File.expand_path("../json_schema_draft_7_schema.json", __FILE__)))
|
56
|
+
schema = ::ElasticGraph::Support::HashUtil.deep_merge(schema, overrides) unless overrides.empty?
|
57
|
+
|
58
|
+
# The meta schema allows additionalProperties in nearly every place. While a JSON schema definition
|
59
|
+
# with additional properties is considered valid, we do not intend to use any additional properties,
|
60
|
+
# and any usage of an additional property is almost certainly a typo. So here we set
|
61
|
+
# `with_unknown_properties_disallowed`.
|
62
|
+
root_schema = ValidatorFactory.new(schema: schema, sanitize_pii: false) # The meta schema has no PII
|
63
|
+
.with_unknown_properties_disallowed
|
64
|
+
.root_schema
|
65
|
+
|
66
|
+
Validator.new(schema: root_schema, sanitize_pii: false)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# Copyright 2024 - 2025 Block, Inc.
|
2
|
+
#
|
3
|
+
# Use of this source code is governed by an MIT-style
|
4
|
+
# license that can be found in the LICENSE file or at
|
5
|
+
# https://opensource.org/licenses/MIT.
|
6
|
+
#
|
7
|
+
# frozen_string_literal: true
|
8
|
+
|
9
|
+
require "elastic_graph/support/memoizable_data"
|
10
|
+
require "json"
|
11
|
+
|
12
|
+
module ElasticGraph
|
13
|
+
module Support
|
14
|
+
module JSONSchema
|
15
|
+
# Responsible for validating JSON data against the ElasticGraph JSON schema for a particular type.
|
16
|
+
#
|
17
|
+
# @!attribute [r] schema
|
18
|
+
# @return [Hash<String, Object>] a JSON schema
|
19
|
+
# @!attribute [r] sanitize_pii
|
20
|
+
# @return [Boolean] whether to omit data that may contain PII from error messages
|
21
|
+
class Validator < MemoizableData.define(:schema, :sanitize_pii)
|
22
|
+
# Validates the given data against the JSON schema, returning true if the data is valid.
|
23
|
+
#
|
24
|
+
# @param data [Object] JSON data to validate
|
25
|
+
# @return [Boolean] true if the data is valid; false if it is invalid
|
26
|
+
#
|
27
|
+
# @see #validate
|
28
|
+
# @see #validate_with_error_message
|
29
|
+
def valid?(data)
|
30
|
+
schema.valid?(data)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Validates the given data against the JSON schema, returning an array of error objects for
|
34
|
+
# any validation errors.
|
35
|
+
#
|
36
|
+
# @param data [Object] JSON data to validate
|
37
|
+
# @return [Array<Hash<String, Object>>] validation errors; will be empty if `data` is valid
|
38
|
+
#
|
39
|
+
# @see #valid?
|
40
|
+
# @see #validate_with_error_message
|
41
|
+
def validate(data)
|
42
|
+
schema.validate(data).map do |error|
|
43
|
+
# The schemas can be very large and make the output very noisy, hiding what matters. So we remove them here.
|
44
|
+
error.delete("root_schema")
|
45
|
+
error.delete("schema")
|
46
|
+
error
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Validates the given data against the JSON schema, returning an error message string if it is invalid.
|
51
|
+
# The error message is intended to be usable to include in a log message or a raised error.
|
52
|
+
#
|
53
|
+
# @param data [Object] JSON data to validate
|
54
|
+
# @return [nil, String] a validation error message, if the data is invalid
|
55
|
+
#
|
56
|
+
# @note The returned error message may contain PII unless {#sanitize_pii} has not been set.
|
57
|
+
#
|
58
|
+
# @see #valid?
|
59
|
+
# @see #validate
|
60
|
+
def validate_with_error_message(data)
|
61
|
+
errors = validate(data)
|
62
|
+
return if errors.empty?
|
63
|
+
|
64
|
+
errors.each { |error| error.delete("data") } if sanitize_pii
|
65
|
+
|
66
|
+
"Validation errors:\n\n#{errors.map { |e| ::JSON.pretty_generate(e) }.join("\n\n")}"
|
67
|
+
end
|
68
|
+
|
69
|
+
# Merges default values defined in the JSON schema into the given `data` without mutating it.
|
70
|
+
#
|
71
|
+
# @param data [Object] JSON data to populate with defaults from the schema
|
72
|
+
# @return [Object] data updated with defaults for missing properties
|
73
|
+
def merge_defaults(data)
|
74
|
+
::Marshal.load(::Marshal.dump(data)).tap do |deep_duped_data|
|
75
|
+
defaulting_schema.valid?(deep_duped_data)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def defaulting_schema
|
82
|
+
@defaulting_schema ||= begin
|
83
|
+
config = schema.configuration.dup
|
84
|
+
config.insert_property_defaults = true
|
85
|
+
::JSONSchemer.schema(schema.value, configuration: config)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# Copyright 2024 - 2025 Block, Inc.
|
2
|
+
#
|
3
|
+
# Use of this source code is governed by an MIT-style
|
4
|
+
# license that can be found in the LICENSE file or at
|
5
|
+
# https://opensource.org/licenses/MIT.
|
6
|
+
#
|
7
|
+
# frozen_string_literal: true
|
8
|
+
|
9
|
+
require "elastic_graph/support/json_schema/validator"
|
10
|
+
require "json_schemer"
|
11
|
+
|
12
|
+
module ElasticGraph
|
13
|
+
module Support
|
14
|
+
module JSONSchema
|
15
|
+
# Factory class responsible for creating {Validator}s for particular ElasticGraph types.
|
16
|
+
class ValidatorFactory
|
17
|
+
# @dynamic root_schema
|
18
|
+
# @private
|
19
|
+
attr_reader :root_schema
|
20
|
+
|
21
|
+
# @param schema [Hash<String, Object>] the JSON schema for an entire ElasticGraph schema
|
22
|
+
# @param sanitize_pii [Boolean] whether to omit data that may contain PII from error messages
|
23
|
+
def initialize(schema:, sanitize_pii:)
|
24
|
+
@raw_schema = schema
|
25
|
+
@root_schema = ::JSONSchemer.schema(
|
26
|
+
schema,
|
27
|
+
meta_schema: schema.fetch("$schema"),
|
28
|
+
# Here we opt to have regular expressions resolved using an ecma-compatible resolver, instead of Ruby's.
|
29
|
+
#
|
30
|
+
# We do this because regexp patterns in our JSON schema are intended to be used by JSON schema libraries
|
31
|
+
# in many languages, not just in Ruby, and we want to support the widest compatibility. For example,
|
32
|
+
# Ruby requires that we use `\A` and `\z` to anchor the start and end of the string (`^` and `$` anchor the
|
33
|
+
# start and end of a line instead), where as ecma regexes treat `^` and `$` as the start and end of the string.
|
34
|
+
# For a pattern to be usable by non-Ruby publishers, we need to use `^/`$` for our start/end anchors, and we
|
35
|
+
# want our validator to treat it the same way here.
|
36
|
+
#
|
37
|
+
# Also, this was the default before json_schemer 1.0 (and we used 0.x versions for a long time...).
|
38
|
+
# This maintains the historical behavior we've had.
|
39
|
+
#
|
40
|
+
# For more info:
|
41
|
+
# https://github.com/davishmcclurg/json_schemer/blob/v1.0.0/CHANGELOG.md#breaking-changes
|
42
|
+
regexp_resolver: "ecma"
|
43
|
+
)
|
44
|
+
|
45
|
+
@sanitize_pii = sanitize_pii
|
46
|
+
@validators_by_type_name = ::Hash.new do |hash, key|
|
47
|
+
hash[key] = Validator.new(
|
48
|
+
schema: root_schema.ref("#/$defs/#{key}"),
|
49
|
+
sanitize_pii: sanitize_pii
|
50
|
+
)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Gets the {Validator} for a particular ElasticGraph type.
|
55
|
+
#
|
56
|
+
# @param type_name [String] name of an ElasticGraph type
|
57
|
+
# @return [Validator]
|
58
|
+
def validator_for(type_name)
|
59
|
+
@validators_by_type_name[type_name] # : Validator
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns a new factory configured to disallow unknown properties. By default, JSON schema
|
63
|
+
# allows unknown properties (they'll simply be ignored when validating a JSON document). It
|
64
|
+
# can be useful to validate more strictly, so that a document with properties not defined in
|
65
|
+
# the JSON schema gets validation errors.
|
66
|
+
#
|
67
|
+
# @param except [Array<String>] paths under which unknown properties should still be allowed
|
68
|
+
# @return [ValidatorFactory]
|
69
|
+
def with_unknown_properties_disallowed(except: [])
|
70
|
+
allow_paths = except.map { |p| p.split(".") }
|
71
|
+
schema_copy = ::Marshal.load(::Marshal.dump(@raw_schema)) # deep copy so our mutations don't affect caller
|
72
|
+
prevent_unknown_properties!(schema_copy, allow_paths: allow_paths)
|
73
|
+
|
74
|
+
ValidatorFactory.new(schema: schema_copy, sanitize_pii: @sanitize_pii)
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# The meta schema allows additionalProperties in nearly every place. While a JSON schema definition
|
80
|
+
# with additional properties is considered valid, we do not intend to use any additional properties,
|
81
|
+
# and any usage of an additional property is almost certainly a typo. So here we mutate the meta
|
82
|
+
# schema to set `additionalProperties: false` everywhere.
|
83
|
+
def prevent_unknown_properties!(object, allow_paths:, parent_path: [])
|
84
|
+
case object
|
85
|
+
when ::Array
|
86
|
+
object.each { |value| prevent_unknown_properties!(value, allow_paths: allow_paths, parent_path: parent_path) }
|
87
|
+
when ::Hash
|
88
|
+
if object["properties"]
|
89
|
+
object["additionalProperties"] = false
|
90
|
+
|
91
|
+
allowed_extra_props = allow_paths.filter_map do |path|
|
92
|
+
*prefix, prop_name = path
|
93
|
+
prop_name if prefix == parent_path
|
94
|
+
end
|
95
|
+
|
96
|
+
allowed_extra_props.each_with_object(object["properties"]) do |prop_name, props|
|
97
|
+
# @type var empty_hash: ::Hash[::String, untyped]
|
98
|
+
# @type var props: untyped
|
99
|
+
empty_hash = {}
|
100
|
+
props[prop_name] ||= empty_hash
|
101
|
+
end
|
102
|
+
|
103
|
+
object["properties"].each do |key, value|
|
104
|
+
prevent_unknown_properties!(value, allow_paths: allow_paths, parent_path: parent_path + [key])
|
105
|
+
end
|
106
|
+
else
|
107
|
+
object.each do |key, value|
|
108
|
+
prevent_unknown_properties!(value, allow_paths: allow_paths, parent_path: parent_path)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -6,6 +6,7 @@
|
|
6
6
|
#
|
7
7
|
# frozen_string_literal: true
|
8
8
|
|
9
|
+
require "elastic_graph/support/config"
|
9
10
|
require "elastic_graph/errors"
|
10
11
|
require "json"
|
11
12
|
require "logger"
|
@@ -17,17 +18,55 @@ module ElasticGraph
|
|
17
18
|
module Logger
|
18
19
|
# Builds a logger instance from the given parsed YAML config.
|
19
20
|
def self.from_parsed_yaml(parsed_yaml)
|
20
|
-
Factory.build(config: Config.from_parsed_yaml(parsed_yaml))
|
21
|
+
Factory.build(config: Config.from_parsed_yaml(parsed_yaml) || Config.new)
|
21
22
|
end
|
22
23
|
|
23
24
|
# @private
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
25
|
+
class Config < Support::Config.define(:level, :device, :formatter)
|
26
|
+
# @dynamic self.from_parsed_yaml
|
27
|
+
|
28
|
+
json_schema at: "logger",
|
29
|
+
optional: false,
|
30
|
+
description: "Configuration for logging used by all parts of ElasticGraph.",
|
31
|
+
properties: {
|
32
|
+
level: {
|
33
|
+
description: "Determines what severity level we log.",
|
34
|
+
examples: %w[INFO WARN],
|
35
|
+
enum: %w[DEBUG debug INFO info WARN warn ERROR error FATAL fatal UNKNOWN unknown],
|
36
|
+
default: "INFO"
|
37
|
+
},
|
38
|
+
device: {
|
39
|
+
description: 'Determines where we log to. "stdout" or "stderr" are interpreted ' \
|
40
|
+
"as being those output streams; any other value is assumed to be a file path.",
|
41
|
+
examples: %w[stdout logs/development.log],
|
42
|
+
default: "stdout",
|
43
|
+
type: "string",
|
44
|
+
minLength: 1
|
45
|
+
},
|
46
|
+
formatter: {
|
47
|
+
description: "Class used to format log messages.",
|
48
|
+
examples: %w[ElasticGraph::Support::Logger::JSONAwareFormatter MyAlternateFormatter],
|
49
|
+
type: "string",
|
50
|
+
pattern: /^[A-Z]\w+(::[A-Z]\w+)*$/.source, # https://rubular.com/r/UuqAz4fR3kdMip
|
51
|
+
default: "ElasticGraph::Support::Logger::JSONAwareFormatter"
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
def prepared_device
|
56
|
+
case device
|
57
|
+
when "stdout" then $stdout
|
58
|
+
when "stderr" then $stderr
|
59
|
+
else
|
60
|
+
::Pathname.new(device).parent.mkpath
|
61
|
+
device
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def convert_values(formatter:, level:, device:)
|
68
|
+
formatter = ::Object.const_get(formatter).new
|
69
|
+
{formatter: formatter, level: level, device: device}
|
31
70
|
end
|
32
71
|
end
|
33
72
|
|
@@ -44,42 +83,14 @@ module ElasticGraph
|
|
44
83
|
end
|
45
84
|
|
46
85
|
# @private
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
:device,
|
54
|
-
# Object used to format log messages. Defaults to an instance of `JSONAwareFormatter`.
|
55
|
-
:formatter
|
56
|
-
)
|
57
|
-
def prepared_device
|
58
|
-
case device
|
59
|
-
when "stdout" then $stdout
|
60
|
-
when "stderr" then $stderr
|
61
|
-
else
|
62
|
-
::Pathname.new(device).parent.mkpath
|
63
|
-
device
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
def self.from_parsed_yaml(hash)
|
68
|
-
hash = hash.fetch("logger")
|
69
|
-
extra_keys = hash.keys - EXPECTED_KEYS
|
70
|
-
|
71
|
-
unless extra_keys.empty?
|
72
|
-
raise Errors::ConfigError, "Unknown `logger` config settings: #{extra_keys.join(", ")}"
|
73
|
-
end
|
74
|
-
|
75
|
-
new(
|
76
|
-
level: hash["level"] || "INFO",
|
77
|
-
device: hash.fetch("device"),
|
78
|
-
formatter: ::Object.const_get(hash.fetch("formatter", JSONAwareFormatter.name)).new
|
86
|
+
module Factory
|
87
|
+
def self.build(config:, device: nil)
|
88
|
+
::Logger.new(
|
89
|
+
device || config.prepared_device,
|
90
|
+
level: config.level,
|
91
|
+
formatter: config.formatter
|
79
92
|
)
|
80
93
|
end
|
81
|
-
|
82
|
-
EXPECTED_KEYS = members.map(&:to_s)
|
83
94
|
end
|
84
95
|
end
|
85
96
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: elasticgraph-support
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Myron Marston
|
@@ -25,6 +25,20 @@ dependencies:
|
|
25
25
|
- - "~>"
|
26
26
|
- !ruby/object:Gem::Version
|
27
27
|
version: '1.7'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: json_schemer
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '2.4'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '2.4'
|
28
42
|
- !ruby/object:Gem::Dependency
|
29
43
|
name: faraday
|
30
44
|
requirement: !ruby/object:Gem::Requirement
|
@@ -55,11 +69,16 @@ files:
|
|
55
69
|
- README.md
|
56
70
|
- lib/elastic_graph/constants.rb
|
57
71
|
- lib/elastic_graph/errors.rb
|
72
|
+
- lib/elastic_graph/support/config.rb
|
58
73
|
- lib/elastic_graph/support/faraday_middleware/msearch_using_get_instead_of_post.rb
|
59
74
|
- lib/elastic_graph/support/faraday_middleware/support_timeouts.rb
|
60
75
|
- lib/elastic_graph/support/from_yaml_file.rb
|
61
76
|
- lib/elastic_graph/support/graphql_formatter.rb
|
62
77
|
- lib/elastic_graph/support/hash_util.rb
|
78
|
+
- lib/elastic_graph/support/json_schema/json_schema_draft_7_schema.json
|
79
|
+
- lib/elastic_graph/support/json_schema/meta_schema_validator.rb
|
80
|
+
- lib/elastic_graph/support/json_schema/validator.rb
|
81
|
+
- lib/elastic_graph/support/json_schema/validator_factory.rb
|
63
82
|
- lib/elastic_graph/support/logger.rb
|
64
83
|
- lib/elastic_graph/support/memoizable_data.rb
|
65
84
|
- lib/elastic_graph/support/monotonic_clock.rb
|
@@ -73,10 +92,10 @@ licenses:
|
|
73
92
|
- MIT
|
74
93
|
metadata:
|
75
94
|
bug_tracker_uri: https://github.com/block/elasticgraph/issues
|
76
|
-
changelog_uri: https://github.com/block/elasticgraph/releases/tag/v1.0.
|
77
|
-
documentation_uri: https://block.github.io/elasticgraph/api-docs/v1.0.
|
95
|
+
changelog_uri: https://github.com/block/elasticgraph/releases/tag/v1.0.1
|
96
|
+
documentation_uri: https://block.github.io/elasticgraph/api-docs/v1.0.1/
|
78
97
|
homepage_uri: https://block.github.io/elasticgraph/
|
79
|
-
source_code_uri: https://github.com/block/elasticgraph/tree/v1.0.
|
98
|
+
source_code_uri: https://github.com/block/elasticgraph/tree/v1.0.1/elasticgraph-support
|
80
99
|
gem_category: core
|
81
100
|
rdoc_options: []
|
82
101
|
require_paths:
|