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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a487f8ccb19ddb241e75c7cb4d559f8a8256561caa9bbcb30afb1ad7f5a79ff2
4
- data.tar.gz: 2eeab837c3c3496d90661a811c8d99f49572b309567f6fdc7abf8c55d38ffb32
3
+ metadata.gz: 389ea95870f3da886ff56a807f488dd15bc9a7fb530e739a8f832392db2cf898
4
+ data.tar.gz: 39e463b98ef99ec346895631686af930e0a249232b2fba12c93763d75983215a
5
5
  SHA512:
6
- metadata.gz: e76c912b45e9c73bec9561395a3beab493f6c728f59ef9f15cbcbabebcc95db06e68d74353b1ac4e1f6776084c8b64e9572feee6ebc8c041639cafeeb058f4c5
7
- data.tar.gz: 2574a6ea55d5883bfe0b1a0ecef83208a170dbf1a6308e6ea96e5d2796ea1d5d90832834025e1e706f5cb4d3616c5d270d605b407a687249162f2bc4943614cf
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
- module Factory
25
- def self.build(config:, device: nil)
26
- ::Logger.new(
27
- device || config.prepared_device,
28
- level: config.level,
29
- formatter: config.formatter
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
- class Config < ::Data.define(
48
- # Determines what severity level we log. Valid values are `DEBUG`, `INFO`, `WARN`,
49
- # `ERROR`, `FATAL` and `UNKNOWN`.
50
- :level,
51
- # Determines where we log to. Must be a string. "stdout" or "stderr" are interpreted
52
- # as being those output streams; any other value is assumed to be a file path.
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
@@ -8,7 +8,7 @@
8
8
 
9
9
  module ElasticGraph
10
10
  # The version of all ElasticGraph gems.
11
- VERSION = "1.0.0.rc4"
11
+ VERSION = "1.0.1"
12
12
 
13
13
  # Steep weirdly expects this here...
14
14
  # @dynamic self.define_schema
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.0.rc4
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.0.rc4
77
- documentation_uri: https://block.github.io/elasticgraph/api-docs/v1.0.0.rc4/
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.0.rc4/elasticgraph-support
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: