elasticgraph-schema_artifacts 0.18.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +3 -0
- data/elasticgraph-schema_artifacts.gemspec +21 -0
- data/lib/elastic_graph/schema_artifacts/artifacts_helper_methods.rb +34 -0
- data/lib/elastic_graph/schema_artifacts/from_disk.rb +112 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/computation_detail.rb +34 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/enum.rb +65 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/extension.rb +51 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/extension_loader.rb +125 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/graphql_field.rb +54 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/hash_dumper.rb +21 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/index_definition.rb +78 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/index_field.rb +33 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/object_type.rb +81 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/params.rb +81 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/relation.rb +39 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/scalar_type.rb +94 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/schema.rb +99 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/schema_element_names.rb +165 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/sort_field.rb +47 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/update_target.rb +80 -0
- metadata +273 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 46a031fd35c0ba70bd94ba54e3fc358c8ab9e4a24d8aceed0aeaab10902f5098
|
4
|
+
data.tar.gz: c3ceaf30c58124c56ecfd333822089b64a1c67d91d32a6c41ebc7e04f89ec018
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: '08e17a7cfd0c2a8a132ea8d4a26a58c0e55a861ad204805cedc13859eacd276e20a02c2adf69099e72039064fab74144fac7c6b3eb9a26b97404cde58acb8d0a'
|
7
|
+
data.tar.gz: 1f4d48bf707f9160cf4d62d966d5a60724503baf3576d1fd5058caaafcb6ac0a592d7636144d5563c462ca9b02eb64abeb6d6988e522bc2509b148d88b874f54
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Block, Inc.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# Copyright 2024 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_relative "../gemspec_helper"
|
10
|
+
|
11
|
+
ElasticGraphGemspecHelper.define_elasticgraph_gem(gemspec_file: __FILE__, category: :core) do |spec, eg_version|
|
12
|
+
spec.summary = "ElasticGraph gem containing code related to generated schema artifacts."
|
13
|
+
|
14
|
+
spec.add_dependency "elasticgraph-support", eg_version
|
15
|
+
|
16
|
+
# Necessary since `ScalarType` references coercion adapters defined in the `elasticgraph-graphql` gem.
|
17
|
+
spec.add_development_dependency "elasticgraph-graphql", eg_version
|
18
|
+
|
19
|
+
# Necessary since `ScalarType` references indexing preparer defined in the `elasticgraph-indexer` gem.
|
20
|
+
spec.add_development_dependency "elasticgraph-indexer", eg_version
|
21
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# Copyright 2024 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
|
+
module ElasticGraph
|
10
|
+
module SchemaArtifacts
|
11
|
+
# Mixin that offers convenient helper methods on top of the basic schema artifacts.
|
12
|
+
# Intended to be mixed into every implementation of the `_SchemaArtifacts` interface.
|
13
|
+
module ArtifactsHelperMethods
|
14
|
+
def datastore_scripts
|
15
|
+
datastore_config.fetch("scripts")
|
16
|
+
end
|
17
|
+
|
18
|
+
def index_templates
|
19
|
+
datastore_config.fetch("index_templates")
|
20
|
+
end
|
21
|
+
|
22
|
+
def indices
|
23
|
+
datastore_config.fetch("indices")
|
24
|
+
end
|
25
|
+
|
26
|
+
# Builds a map of index mappings, keyed by index definition name.
|
27
|
+
def index_mappings_by_index_def_name
|
28
|
+
@index_mappings_by_index_def_name ||= index_templates
|
29
|
+
.transform_values { |config| config.fetch("template").fetch("mappings") }
|
30
|
+
.merge(indices.transform_values { |config| config.fetch("mappings") })
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# Copyright 2024 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/constants"
|
10
|
+
require "elastic_graph/error"
|
11
|
+
require "elastic_graph/schema_artifacts/artifacts_helper_methods"
|
12
|
+
require "elastic_graph/schema_artifacts/runtime_metadata/schema"
|
13
|
+
require "elastic_graph/support/hash_util"
|
14
|
+
require "elastic_graph/support/memoizable_data"
|
15
|
+
require "yaml"
|
16
|
+
|
17
|
+
module ElasticGraph
|
18
|
+
module SchemaArtifacts
|
19
|
+
# Builds a `SchemaArtifacts::FromDisk` instance using the provided YAML settings.
|
20
|
+
def self.from_parsed_yaml(parsed_yaml, for_context:)
|
21
|
+
schema_artifacts = parsed_yaml.fetch("schema_artifacts") do
|
22
|
+
raise ConfigError, "Config is missing required key `schema_artifacts`."
|
23
|
+
end
|
24
|
+
|
25
|
+
if (extra_keys = schema_artifacts.keys - ["directory"]).any?
|
26
|
+
raise ConfigError, "Config has extra `schema_artifacts` keys: #{extra_keys}"
|
27
|
+
end
|
28
|
+
|
29
|
+
directory = schema_artifacts.fetch("directory") do
|
30
|
+
raise ConfigError, "Config is missing required key `schema_artifacts.directory`."
|
31
|
+
end
|
32
|
+
|
33
|
+
FromDisk.new(directory, for_context)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Responsible for loading schema artifacts from disk.
|
37
|
+
class FromDisk < Support::MemoizableData.define(:artifacts_dir, :context)
|
38
|
+
include ArtifactsHelperMethods
|
39
|
+
|
40
|
+
def graphql_schema_string
|
41
|
+
@graphql_schema_string ||= read_artifact(GRAPHQL_SCHEMA_FILE)
|
42
|
+
end
|
43
|
+
|
44
|
+
def json_schemas_for(version)
|
45
|
+
unless available_json_schema_versions.include?(version)
|
46
|
+
raise MissingSchemaArtifactError, "The requested json schema version (#{version}) is not available. " \
|
47
|
+
"Available versions: #{available_json_schema_versions.sort.join(", ")}."
|
48
|
+
end
|
49
|
+
|
50
|
+
json_schemas_by_version[version]
|
51
|
+
end
|
52
|
+
|
53
|
+
def available_json_schema_versions
|
54
|
+
@available_json_schema_versions ||= begin
|
55
|
+
versioned_json_schemas_dir = ::File.join(artifacts_dir, JSON_SCHEMAS_BY_VERSION_DIRECTORY)
|
56
|
+
if ::Dir.exist?(versioned_json_schemas_dir)
|
57
|
+
::Dir.entries(versioned_json_schemas_dir).filter_map { |it| it[/v(\d+)\.yaml/, 1]&.to_i }.to_set
|
58
|
+
else
|
59
|
+
::Set.new
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def latest_json_schema_version
|
65
|
+
@latest_json_schema_version ||= available_json_schema_versions.max || raise(
|
66
|
+
MissingSchemaArtifactError,
|
67
|
+
"The directory for versioned JSON schemas (#{::File.join(artifacts_dir, JSON_SCHEMAS_BY_VERSION_DIRECTORY)}) could not be found. " \
|
68
|
+
"Either the schema artifacts haven't been dumped yet or the schema artifacts directory (#{artifacts_dir}) is misconfigured."
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
def datastore_config
|
73
|
+
@datastore_config ||= _ = parsed_yaml_from(DATASTORE_CONFIG_FILE)
|
74
|
+
end
|
75
|
+
|
76
|
+
def runtime_metadata
|
77
|
+
@runtime_metadata ||= RuntimeMetadata::Schema.from_hash(
|
78
|
+
parsed_yaml_from(RUNTIME_METADATA_FILE),
|
79
|
+
for_context: context
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def read_artifact(artifact_name)
|
86
|
+
file_name = ::File.join(artifacts_dir, artifact_name)
|
87
|
+
|
88
|
+
if ::File.exist?(file_name)
|
89
|
+
::File.read(file_name)
|
90
|
+
else
|
91
|
+
raise MissingSchemaArtifactError, "Schema artifact `#{artifact_name}` could not be found. " \
|
92
|
+
"Either the schema artifacts haven't been dumped yet or the schema artifacts directory (#{artifacts_dir}) is misconfigured."
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def parsed_yaml_from(artifact_name)
|
97
|
+
::YAML.safe_load(read_artifact(artifact_name))
|
98
|
+
end
|
99
|
+
|
100
|
+
def json_schemas_by_version
|
101
|
+
@json_schemas_by_version ||= ::Hash.new do |hash, json_schema_version|
|
102
|
+
hash[json_schema_version] = load_json_schema(json_schema_version)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Loads the given JSON schema version from disk.
|
107
|
+
def load_json_schema(json_schema_version)
|
108
|
+
parsed_yaml_from(::File.join(JSON_SCHEMAS_BY_VERSION_DIRECTORY, "v#{json_schema_version}.yaml"))
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# Copyright 2024 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
|
+
module ElasticGraph
|
10
|
+
module SchemaArtifacts
|
11
|
+
module RuntimeMetadata
|
12
|
+
# Details about our aggregation functions.
|
13
|
+
class ComputationDetail < ::Data.define(:empty_bucket_value, :function)
|
14
|
+
FUNCTION = "function"
|
15
|
+
EMPTY_BUCKET_VALUE = "empty_bucket_value"
|
16
|
+
|
17
|
+
def self.from_hash(hash)
|
18
|
+
new(
|
19
|
+
empty_bucket_value: hash[EMPTY_BUCKET_VALUE],
|
20
|
+
function: hash.fetch(FUNCTION).to_sym
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_dumpable_hash
|
25
|
+
{
|
26
|
+
# Keys here are ordered alphabetically; please keep them that way.
|
27
|
+
EMPTY_BUCKET_VALUE => empty_bucket_value,
|
28
|
+
FUNCTION => function.to_s
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# Copyright 2024 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/schema_artifacts/runtime_metadata/hash_dumper"
|
10
|
+
require "elastic_graph/schema_artifacts/runtime_metadata/sort_field"
|
11
|
+
|
12
|
+
module ElasticGraph
|
13
|
+
module SchemaArtifacts
|
14
|
+
module RuntimeMetadata
|
15
|
+
module Enum
|
16
|
+
# Runtime metadata related to an ElasticGraph enum type.
|
17
|
+
class Type < ::Data.define(:values_by_name)
|
18
|
+
VALUES_BY_NAME = "values_by_name"
|
19
|
+
|
20
|
+
def self.from_hash(hash)
|
21
|
+
values_by_name = hash[VALUES_BY_NAME]&.transform_values do |value_hash|
|
22
|
+
Value.from_hash(value_hash)
|
23
|
+
end || {}
|
24
|
+
|
25
|
+
new(values_by_name: values_by_name)
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_dumpable_hash
|
29
|
+
{
|
30
|
+
# Keys here are ordered alphabetically; please keep them that way.
|
31
|
+
VALUES_BY_NAME => HashDumper.dump_hash(values_by_name, &:to_dumpable_hash)
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Runtime metadata related to an ElasticGraph enum value.
|
37
|
+
class Value < ::Data.define(:sort_field, :datastore_value, :datastore_abbreviation, :alternate_original_name)
|
38
|
+
DATASTORE_VALUE = "datastore_value"
|
39
|
+
DATASTORE_ABBREVIATION = "datastore_abbreviation"
|
40
|
+
SORT_FIELD = "sort_field"
|
41
|
+
ALTERNATE_ORIGINAL_NAME = "alternate_original_name"
|
42
|
+
|
43
|
+
def self.from_hash(hash)
|
44
|
+
new(
|
45
|
+
sort_field: hash[SORT_FIELD]&.then { |h| SortField.from_hash(h) },
|
46
|
+
datastore_value: hash[DATASTORE_VALUE],
|
47
|
+
datastore_abbreviation: hash[DATASTORE_ABBREVIATION]&.to_sym,
|
48
|
+
alternate_original_name: hash[ALTERNATE_ORIGINAL_NAME]
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_dumpable_hash
|
53
|
+
{
|
54
|
+
# Keys here are ordered alphabetically; please keep them that way.
|
55
|
+
DATASTORE_ABBREVIATION => datastore_abbreviation&.to_s,
|
56
|
+
DATASTORE_VALUE => datastore_value,
|
57
|
+
ALTERNATE_ORIGINAL_NAME => alternate_original_name,
|
58
|
+
SORT_FIELD => sort_field&.to_dumpable_hash
|
59
|
+
}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# Copyright 2024 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/hash_util"
|
10
|
+
|
11
|
+
module ElasticGraph
|
12
|
+
module SchemaArtifacts
|
13
|
+
module RuntimeMetadata
|
14
|
+
# Represents an extension--a class or module (potentially from outside the ElasticGraph
|
15
|
+
# code base) that implements a standard interface to plug in custom functionality.
|
16
|
+
#
|
17
|
+
# Extensions are serialized using two fields:
|
18
|
+
# - `extension_name`: the Ruby constant of the extension
|
19
|
+
# - `require_path`: file path to `require` to load the extension
|
20
|
+
#
|
21
|
+
# However, an `Extension` instance represents a loaded, resolved extension.
|
22
|
+
# We eagerly load extensions (and validate them in the `ExtensionLoader`) in
|
23
|
+
# order to surface any issues with the extension as soon as possible. We don't
|
24
|
+
# want to defer errors if we can detect any issues with the extension at boot time.
|
25
|
+
Extension = ::Data.define(:extension_class, :require_path, :extension_config) do
|
26
|
+
# @implements Extension
|
27
|
+
|
28
|
+
# Loads an extension using a serialized hash, via the provided `ExtensionLoader`.
|
29
|
+
def self.load_from_hash(hash, via:)
|
30
|
+
config = Support::HashUtil.symbolize_keys(hash["extension_config"] || {}) # : ::Hash[::Symbol, untyped]
|
31
|
+
via.load(hash.fetch("extension_name"), from: hash.fetch("require_path"), config: config)
|
32
|
+
end
|
33
|
+
|
34
|
+
# The name of the extension (based on the name of the extension class).
|
35
|
+
def extension_name
|
36
|
+
extension_class.name.to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
# The serialized form of an extension.
|
40
|
+
def to_dumpable_hash
|
41
|
+
# Keys here are ordered alphabetically; please keep them that way.
|
42
|
+
{
|
43
|
+
"extension_config" => Support::HashUtil.stringify_keys(extension_config),
|
44
|
+
"extension_name" => extension_name,
|
45
|
+
"require_path" => require_path
|
46
|
+
}.reject { |_, v| v.empty? }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# Copyright 2024 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/error"
|
10
|
+
require "elastic_graph/schema_artifacts/runtime_metadata/extension"
|
11
|
+
|
12
|
+
module ElasticGraph
|
13
|
+
module SchemaArtifacts
|
14
|
+
module RuntimeMetadata
|
15
|
+
# Responsible for loading extensions. This loader requires an interface definition
|
16
|
+
# (a class or module with empty method definitions that just serves to define what
|
17
|
+
# loaded extensions must implement). That allows us to verify the extension implements
|
18
|
+
# the interface correctly at load time, rather than deferring exceptions to when the
|
19
|
+
# extension is later used.
|
20
|
+
#
|
21
|
+
# Note, however, that this does not guarantee no runtime exceptions from the use of the
|
22
|
+
# extension: the extension may return invalid return values, or throw exceptions when
|
23
|
+
# called. But this verifies the interface to the extent that we can.
|
24
|
+
class ExtensionLoader
|
25
|
+
def initialize(interface_def)
|
26
|
+
@interface_def = interface_def
|
27
|
+
@loaded_by_name = {}
|
28
|
+
end
|
29
|
+
|
30
|
+
# Loads the extension using the provided constant name, after requiring the `from` path.
|
31
|
+
# Memoizes the result.
|
32
|
+
def load(constant_name, from:, config:)
|
33
|
+
(@loaded_by_name[constant_name] ||= load_extension(constant_name, from)).tap do |extension|
|
34
|
+
if extension.require_path != from
|
35
|
+
raise InvalidExtensionError, "Extension `#{constant_name}` cannot be loaded from `#{from}`, " \
|
36
|
+
"since it has already been loaded from `#{extension.require_path}`."
|
37
|
+
end
|
38
|
+
end.with(extension_config: config)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def load_extension(constant_name, require_path)
|
44
|
+
require require_path
|
45
|
+
extension_class = ::Object.const_get(constant_name).tap { |ext| verify_interface(constant_name, ext) }
|
46
|
+
Extension.new(extension_class, require_path, {})
|
47
|
+
end
|
48
|
+
|
49
|
+
def verify_interface(constant_name, extension)
|
50
|
+
# @type var problems: ::Array[::String]
|
51
|
+
problems = []
|
52
|
+
problems.concat(verify_methods("class", extension.singleton_class, @interface_def.singleton_class))
|
53
|
+
|
54
|
+
if extension.is_a?(::Module)
|
55
|
+
problems.concat(verify_methods("instance", extension, @interface_def))
|
56
|
+
|
57
|
+
# We care about the name exactly matching so that we can dump the extension name in a schema
|
58
|
+
# artifact w/o having to pass around the original constant name.
|
59
|
+
if extension.name != constant_name.delete_prefix("::")
|
60
|
+
problems << "- Exposes a name (`#{extension.name}`) that differs from the provided extension name (`#{constant_name}`)"
|
61
|
+
end
|
62
|
+
else
|
63
|
+
problems << "- Is not a class or module as expected"
|
64
|
+
end
|
65
|
+
|
66
|
+
if problems.any?
|
67
|
+
raise InvalidExtensionError,
|
68
|
+
"Extension `#{constant_name}` does not implement the expected interface correctly. Problems:\n\n" \
|
69
|
+
"#{problems.join("\n")}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def verify_methods(type, extension, interface)
|
74
|
+
interface_methods = list_instance_interface_methods(interface)
|
75
|
+
extension_methods = list_instance_interface_methods(extension)
|
76
|
+
|
77
|
+
# @type var problems: ::Array[::String]
|
78
|
+
problems = []
|
79
|
+
|
80
|
+
if (missing_methods = interface_methods - extension_methods).any?
|
81
|
+
problems << "- Missing #{type} methods: #{missing_methods.map { |m| "`#{m}`" }.join(", ")}"
|
82
|
+
end
|
83
|
+
|
84
|
+
interface_methods.intersection(extension_methods).each do |method_name|
|
85
|
+
unless parameters_match?(extension, interface, method_name)
|
86
|
+
interface_signature = signature_code_for(interface, method_name)
|
87
|
+
extension_signature = signature_code_for(extension, method_name)
|
88
|
+
|
89
|
+
problems << "- Method signature for #{type} method `#{method_name}` (`#{extension_signature}`) does not match interface (`#{interface_signature}`)"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
problems
|
94
|
+
end
|
95
|
+
|
96
|
+
def list_instance_interface_methods(klass)
|
97
|
+
# Here we look at more than just the public methods. This is necessary for `initialize`.
|
98
|
+
# If it's defined on the interface definition, we want to verify it on the extension,
|
99
|
+
# but Ruby makes `initialize` private by default.
|
100
|
+
klass.instance_methods(false) +
|
101
|
+
klass.protected_instance_methods(false) +
|
102
|
+
klass.private_instance_methods(false)
|
103
|
+
end
|
104
|
+
|
105
|
+
def parameters_match?(extension, interface, method_name)
|
106
|
+
interface_parameters = interface.instance_method(method_name).parameters
|
107
|
+
extension_parameters = extension.instance_method(method_name).parameters
|
108
|
+
|
109
|
+
# Here we compare the parameters for exact equality. This is stricter than we need it
|
110
|
+
# to be (it doesn't allow the parameters to have different names, for example) but it's
|
111
|
+
# considerably simpler than us trying to determine what is truly required. For example,
|
112
|
+
# the name doesn't matter on a positional arg, but would matter on a keyword arg.
|
113
|
+
interface_parameters == extension_parameters
|
114
|
+
end
|
115
|
+
|
116
|
+
def signature_code_for(object, method_name)
|
117
|
+
# @type var file_name: ::String?
|
118
|
+
# @type var line_number: ::Integer?
|
119
|
+
file_name, line_number = object.instance_method(method_name).source_location
|
120
|
+
::File.read(file_name.to_s).split("\n")[line_number.to_i - 1].strip
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# Copyright 2024 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/schema_artifacts/runtime_metadata/computation_detail"
|
10
|
+
require "elastic_graph/schema_artifacts/runtime_metadata/relation"
|
11
|
+
|
12
|
+
module ElasticGraph
|
13
|
+
module SchemaArtifacts
|
14
|
+
module RuntimeMetadata
|
15
|
+
class GraphQLField < ::Data.define(:name_in_index, :relation, :computation_detail)
|
16
|
+
EMPTY = new(nil, nil, nil)
|
17
|
+
NAME_IN_INDEX = "name_in_index"
|
18
|
+
RELATION = "relation"
|
19
|
+
AGGREGATION_DETAIL = "computation_detail"
|
20
|
+
|
21
|
+
def self.from_hash(hash)
|
22
|
+
new(
|
23
|
+
name_in_index: hash[NAME_IN_INDEX],
|
24
|
+
relation: hash[RELATION]&.then { |rel_hash| Relation.from_hash(rel_hash) },
|
25
|
+
computation_detail: hash[AGGREGATION_DETAIL]&.then { |agg_hash| ComputationDetail.from_hash(agg_hash) }
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_dumpable_hash
|
30
|
+
{
|
31
|
+
# Keys here are ordered alphabetically; please keep them that way.
|
32
|
+
AGGREGATION_DETAIL => computation_detail&.to_dumpable_hash,
|
33
|
+
NAME_IN_INDEX => name_in_index,
|
34
|
+
RELATION => relation&.to_dumpable_hash
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
# Indicates if we need this field in our dumped runtime metadata, when it has the given
|
39
|
+
# `name_in_graphql`. Fields that have not been customized in some way do not need to be
|
40
|
+
# included in the dumped runtime metadata.
|
41
|
+
def needed?(name_in_graphql)
|
42
|
+
!!relation || !!computation_detail || name_in_index&.!=(name_in_graphql) || false
|
43
|
+
end
|
44
|
+
|
45
|
+
def with_computation_detail(empty_bucket_value:, function:)
|
46
|
+
with(computation_detail: ComputationDetail.new(
|
47
|
+
empty_bucket_value: empty_bucket_value,
|
48
|
+
function: function
|
49
|
+
))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Copyright 2024 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
|
+
module ElasticGraph
|
10
|
+
module SchemaArtifacts
|
11
|
+
module RuntimeMetadata
|
12
|
+
module HashDumper
|
13
|
+
def self.dump_hash(hash)
|
14
|
+
hash.sort_by(&:first).to_h do |key, value|
|
15
|
+
[key, yield(value)]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# Copyright 2024 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/schema_artifacts/runtime_metadata/hash_dumper"
|
10
|
+
require "elastic_graph/schema_artifacts/runtime_metadata/index_field"
|
11
|
+
require "elastic_graph/schema_artifacts/runtime_metadata/sort_field"
|
12
|
+
|
13
|
+
module ElasticGraph
|
14
|
+
module SchemaArtifacts
|
15
|
+
module RuntimeMetadata
|
16
|
+
# Runtime metadata related to a datastore index definition.
|
17
|
+
class IndexDefinition < ::Data.define(:route_with, :rollover, :default_sort_fields, :current_sources, :fields_by_path)
|
18
|
+
ROUTE_WITH = "route_with"
|
19
|
+
ROLLOVER = "rollover"
|
20
|
+
DEFAULT_SORT_FIELDS = "default_sort_fields"
|
21
|
+
CURRENT_SOURCES = "current_sources"
|
22
|
+
FIELDS_BY_PATH = "fields_by_path"
|
23
|
+
|
24
|
+
def initialize(route_with:, rollover:, default_sort_fields:, current_sources:, fields_by_path:)
|
25
|
+
super(
|
26
|
+
route_with: route_with,
|
27
|
+
rollover: rollover,
|
28
|
+
default_sort_fields: default_sort_fields,
|
29
|
+
current_sources: current_sources.to_set,
|
30
|
+
fields_by_path: fields_by_path
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.from_hash(hash)
|
35
|
+
new(
|
36
|
+
route_with: hash[ROUTE_WITH],
|
37
|
+
rollover: hash[ROLLOVER]&.then { |h| Rollover.from_hash(h) },
|
38
|
+
default_sort_fields: hash[DEFAULT_SORT_FIELDS]&.map { |h| SortField.from_hash(h) } || [],
|
39
|
+
current_sources: hash[CURRENT_SOURCES] || [],
|
40
|
+
fields_by_path: (hash[FIELDS_BY_PATH] || {}).transform_values { |h| IndexField.from_hash(h) }
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_dumpable_hash
|
45
|
+
{
|
46
|
+
# Keys here are ordered alphabetically; please keep them that way.
|
47
|
+
CURRENT_SOURCES => current_sources.sort,
|
48
|
+
DEFAULT_SORT_FIELDS => default_sort_fields.map(&:to_dumpable_hash),
|
49
|
+
FIELDS_BY_PATH => HashDumper.dump_hash(fields_by_path, &:to_dumpable_hash),
|
50
|
+
ROLLOVER => rollover&.to_dumpable_hash,
|
51
|
+
ROUTE_WITH => route_with
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
class Rollover < ::Data.define(:frequency, :timestamp_field_path)
|
56
|
+
FREQUENCY = "frequency"
|
57
|
+
TIMESTAMP_FIELD_PATH = "timestamp_field_path"
|
58
|
+
|
59
|
+
# @implements Rollover
|
60
|
+
def self.from_hash(hash)
|
61
|
+
new(
|
62
|
+
frequency: hash.fetch(FREQUENCY).to_sym,
|
63
|
+
timestamp_field_path: hash[TIMESTAMP_FIELD_PATH]
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_dumpable_hash
|
68
|
+
{
|
69
|
+
# Keys here are ordered alphabetically; please keep them that way.
|
70
|
+
FREQUENCY => frequency.to_s,
|
71
|
+
TIMESTAMP_FIELD_PATH => timestamp_field_path
|
72
|
+
}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# Copyright 2024 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/constants"
|
10
|
+
|
11
|
+
module ElasticGraph
|
12
|
+
module SchemaArtifacts
|
13
|
+
module RuntimeMetadata
|
14
|
+
# Runtime metadata related to a field on a datastore index definition.
|
15
|
+
class IndexField < ::Data.define(:source)
|
16
|
+
SOURCE = "source"
|
17
|
+
|
18
|
+
def self.from_hash(hash)
|
19
|
+
new(
|
20
|
+
source: hash[SOURCE] || SELF_RELATIONSHIP_NAME
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_dumpable_hash
|
25
|
+
{
|
26
|
+
# Keys here are ordered alphabetically; please keep them that way.
|
27
|
+
SOURCE => source
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|