diff --git a/changelog/added/extension-extraordinaire.md b/changelog/added/extension-extraordinaire.md new file mode 100644 index 00000000..fd72a9b2 --- /dev/null +++ b/changelog/added/extension-extraordinaire.md @@ -0,0 +1 @@ +Added experimental envconfig extensions. Pass `extensions:` to `Temporalio::EnvConfig::ClientConfig.load_client_connect_options` to let separately packaged extensions read their own config from `profile..ext` and environment variables and apply it through client plugins. diff --git a/temporalio/ext/src/envconfig.rs b/temporalio/ext/src/envconfig.rs index 3d2b3712..9f3bd44f 100644 --- a/temporalio/ext/src/envconfig.rs +++ b/temporalio/ext/src/envconfig.rs @@ -1,12 +1,12 @@ use std::collections::HashMap; -use magnus::{Error, RHash, RString, Ruby, function, prelude::*, scan_args}; +use magnus::{Error, IntoValue, RHash, RString, Ruby, Value, function, prelude::*, scan_args}; use temporalio_common::envconfig::{ ClientConfig as CoreClientConfig, ClientConfigCodec, ClientConfigProfile as CoreClientConfigProfile, ClientConfigTLS as CoreClientConfigTLS, DataSource, LoadClientConfigOptions, LoadClientConfigProfileOptions, load_client_config as core_load_client_config, - load_client_config_profile as core_load_client_config_profile, + load_client_config_profile as core_load_client_config_profile, toml, }; use crate::{ROOT_MOD, error}; @@ -105,10 +105,45 @@ fn profile_to_hash(ruby: &Ruby, profile: &CoreClientConfigProfile) -> Result Result { + let hash = ruby.hash_new(); + for (k, v) in table { + hash.aset(ruby.str_new(k), toml_value_to_ruby(ruby, v)?)?; + } Ok(hash) } +// Datetimes become ISO 8601 strings (e.g. `1979-05-27T07:32:00Z`, or `1979-05-27` for a local +// date), since Ruby has no native types for TOML's local datetimes and local times. +fn toml_value_to_ruby(ruby: &Ruby, value: &toml::Value) -> Result { + Ok(match value { + toml::Value::String(s) => ruby.str_new(s).as_value(), + toml::Value::Integer(i) => i.into_value_with(ruby), + toml::Value::Float(f) => f.into_value_with(ruby), + toml::Value::Boolean(b) => b.into_value_with(ruby), + toml::Value::Datetime(dt) => ruby.str_new(&dt.to_string()).as_value(), + toml::Value::Array(a) => { + let ary = ruby.ary_new_capa(a.len()); + for v in a { + ary.push(toml_value_to_ruby(ruby, v)?)?; + } + ary.as_value() + } + toml::Value::Table(t) => toml_table_to_hash(ruby, t)?.as_value(), + }) +} + fn core_config_to_hash(ruby: &Ruby, core_config: &CoreClientConfig) -> Result { let profiles_hash = ruby.hash_new(); for (name, profile) in &core_config.profiles { diff --git a/temporalio/lib/temporalio/env_config.rb b/temporalio/lib/temporalio/env_config.rb index 8a2f6c03..c50a4440 100644 --- a/temporalio/lib/temporalio/env_config.rb +++ b/temporalio/lib/temporalio/env_config.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'pathname' +require 'temporalio/error' require 'temporalio/internal/bridge' module Temporalio @@ -136,7 +137,12 @@ def read_source(source) # @return [Boolean, ClientConfigTLS, nil] TLS configuration # @!attribute [r] grpc_meta # @return [Hash] gRPC metadata - ClientConfigProfile = Data.define(:address, :namespace, :api_key, :tls, :grpc_meta) + # @!attribute [r] ext + # @return [Hash>] Raw extension config from +profile..ext+, keyed by + # extension name. Each extension decodes its own table. TOML datetimes are ISO 8601 strings. Not modified by + # environment variables. This should be treated as read-only, since copies of a + # profile may share it. WARNING: Extensions are experimental. + ClientConfigProfile = Data.define(:address, :namespace, :api_key, :tls, :grpc_meta, :ext) # A client configuration profile loaded from environment and files. # @@ -153,7 +159,8 @@ def self.from_h(hash) namespace: hash[:namespace], api_key: hash[:api_key], tls: ClientConfigTLS.from_h(hash[:tls]), - grpc_meta: hash[:grpc_meta] || {} + grpc_meta: hash[:grpc_meta] || {}, + ext: hash[:ext] || {} ) end @@ -185,7 +192,7 @@ def self.load( end # Create a ClientConfigProfile instance with defaults - def initialize(address: nil, namespace: nil, api_key: nil, tls: nil, grpc_meta: {}) + def initialize(address: nil, namespace: nil, api_key: nil, tls: nil, grpc_meta: {}, ext: {}) super end @@ -197,7 +204,8 @@ def to_h namespace: namespace, api_key: api_key, tls: tls&.to_h&.then { |tls_hash| tls_hash.empty? ? nil : tls_hash }, - grpc_meta: grpc_meta && grpc_meta.empty? ? nil : grpc_meta + grpc_meta: grpc_meta && grpc_meta.empty? ? nil : grpc_meta, + ext: ext && ext.empty? ? nil : ext }.compact end @@ -222,6 +230,46 @@ def to_client_connect_options end end + # Extension mixin to include for extending client configuration with extra TOML properties and environment + # variables, and applying them to the client created from the configuration. + # + # Each extension owns a TOML table under +profile..ext.+, e.g.: + # + # [profile.default.ext.my_extension] + # some_field = "value" + # + # Extensions are recommended to use a +TEMPORAL_EXT__+ prefix for any environment variables they read. + # + # WARNING: Extensions are experimental. + module ClientConfigExtension + # @abstract + # @return [String] The extension's table key under +profile..ext+. Must be unique among loaded + # extensions. + def name + raise NotImplementedError + end + + # Read this extension's config from the profile and env, and return a plugin that applies it. + # + # @abstract + # @param request [ClientConfigExtensionRequest] Loaded profile and env. + # @return [Client::Plugin, nil] Plugin, or nil if there's nothing to apply. + def client_plugin(request) + raise NotImplementedError + end + end + + # Input to {ClientConfigExtension#client_plugin}. + # + # WARNING: Extensions are experimental. + # + # @!attribute [r] profile + # @return [ClientConfigProfile] Loaded profile, with environment variable overrides applied. + # @!attribute [r] env + # @return [Hash, nil] Environment variables to read extension env vars from, or nil if + # environment variable loading is disabled. + ClientConfigExtensionRequest = Data.define(:profile, :env) + # Client configuration loaded from TOML and environment variables. # # This contains a mapping of profile names to client profiles. @@ -273,7 +321,7 @@ def self.load( # Load a single client profile and convert to connect config # # This is a convenience function that combines loading a profile and - # converting it to a connect config hash. + # converting it to a connect config hash, plus applying extensions. # # @param profile [String, nil] The profile to load from the config # @param config_source [Pathname, String, nil] Configuration source @@ -281,6 +329,8 @@ def self.load( # @param disable_env [Boolean] If true, environment variable loading and overriding is disabled # @param config_file_strict [Boolean] If true, will error on unrecognized keys # @param override_env_vars [Hash, nil] Environment variables to use for loading and overrides + # @param extensions [Array] Extensions to call, in order, with the loaded profile. Plugins + # they return are included as +plugins+ in the keyword args. WARNING: Extensions are experimental. # @return [Array] Tuple of [positional_args, keyword_args] that can be splatted to Client.connect def self.load_client_connect_options( profile: nil, @@ -288,8 +338,10 @@ def self.load_client_connect_options( disable_file: false, disable_env: false, config_file_strict: false, - override_env_vars: nil + override_env_vars: nil, + extensions: [] ) + _validate_extensions!(extensions) prof = ClientConfigProfile.load( profile: profile, config_source: config_source, @@ -298,7 +350,36 @@ def self.load_client_connect_options( config_file_strict: config_file_strict, override_env_vars: override_env_vars ) - prof.to_client_connect_options + positional_args, keyword_args = prof.to_client_connect_options + return [positional_args, keyword_args] if extensions.empty? + + request = ClientConfigExtensionRequest.new( + profile: prof, + env: disable_env ? nil : override_env_vars || ENV.to_h + ) + plugins = extensions.filter_map do |extension| + extension.client_plugin(request) + rescue StandardError => e + raise Error, "client config extension \"#{extension.name}\": #{e.message}" + end + keyword_args[:plugins] = plugins unless plugins.empty? + [positional_args, keyword_args] + end + + # @!visibility private + def self._validate_extensions!(extensions) + names = {} + extensions.each_with_index do |extension, index| + unless extension.is_a?(ClientConfigExtension) + raise ArgumentError, "#{extension.class} does not implement EnvConfig::ClientConfigExtension" + end + + name = extension.name + raise ArgumentError, "client config extension at index #{index} has empty name" if name.empty? + raise ArgumentError, "duplicate client config extension name \"#{name}\"" if names.key?(name) + + names[name] = true + end end # Create a ClientConfig instance with defaults diff --git a/temporalio/rbi/temporalio/env_config.rbi b/temporalio/rbi/temporalio/env_config.rbi index ca9af2a8..245e8fab 100644 --- a/temporalio/rbi/temporalio/env_config.rbi +++ b/temporalio/rbi/temporalio/env_config.rbi @@ -65,13 +65,17 @@ class Temporalio::EnvConfig::ClientConfigProfile < ::Data sig { returns(T::Hash[String, String]) } def grpc_meta; end + sig { returns(T::Hash[String, T::Hash[String, Object]]) } + def ext; end + sig do params( address: T.nilable(String), namespace: T.nilable(String), api_key: T.nilable(String), tls: T.nilable(Temporalio::EnvConfig::ClientConfigTLS), - grpc_meta: T::Hash[String, String] + grpc_meta: T::Hash[String, String], + ext: T::Hash[String, T::Hash[String, Object]] ).void end def initialize( @@ -79,7 +83,8 @@ class Temporalio::EnvConfig::ClientConfigProfile < ::Data namespace: T.unsafe(nil), api_key: T.unsafe(nil), tls: T.unsafe(nil), - grpc_meta: T.unsafe(nil) + grpc_meta: T.unsafe(nil), + ext: T.unsafe(nil) ); end sig { params(hash: T::Hash[Symbol, Object]).returns(Temporalio::EnvConfig::ClientConfigProfile) } @@ -111,6 +116,34 @@ class Temporalio::EnvConfig::ClientConfigProfile < ::Data def to_client_connect_options; end end +module Temporalio::EnvConfig::ClientConfigExtension + sig { returns(String) } + def name; end + + sig do + params(request: Temporalio::EnvConfig::ClientConfigExtensionRequest).returns(T.nilable(Temporalio::Client::Plugin)) + end + def client_plugin(request); end +end + +class Temporalio::EnvConfig::ClientConfigExtensionRequest < ::Data + extend T::Sig + + sig { returns(Temporalio::EnvConfig::ClientConfigProfile) } + def profile; end + + sig { returns(T.nilable(T::Hash[String, String])) } + def env; end + + sig do + params( + profile: Temporalio::EnvConfig::ClientConfigProfile, + env: T.nilable(T::Hash[String, String]) + ).void + end + def initialize(profile:, env:); end +end + class Temporalio::EnvConfig::ClientConfig < ::Data extend T::Sig @@ -139,7 +172,8 @@ class Temporalio::EnvConfig::ClientConfig < ::Data disable_file: T::Boolean, disable_env: T::Boolean, config_file_strict: T::Boolean, - override_env_vars: T.nilable(T::Hash[String, String]) + override_env_vars: T.nilable(T::Hash[String, String]), + extensions: T::Array[Temporalio::EnvConfig::ClientConfigExtension] ).returns([T::Array[T.nilable(String)], T::Hash[Symbol, Object]]) end def self.load_client_connect_options( @@ -148,7 +182,8 @@ class Temporalio::EnvConfig::ClientConfig < ::Data disable_file: T.unsafe(nil), disable_env: T.unsafe(nil), config_file_strict: T.unsafe(nil), - override_env_vars: T.unsafe(nil) + override_env_vars: T.unsafe(nil), + extensions: T.unsafe(nil) ); end sig { override(allow_incompatible: true).returns(T::Hash[String, T::Hash[Symbol, Object]]) } diff --git a/temporalio/sig/temporalio/env_config.rbs b/temporalio/sig/temporalio/env_config.rbs index dbc3bfe1..2728f58d 100644 --- a/temporalio/sig/temporalio/env_config.rbs +++ b/temporalio/sig/temporalio/env_config.rbs @@ -35,6 +35,7 @@ module Temporalio attr_reader api_key: String? attr_reader tls: ClientConfigTLS? attr_reader grpc_meta: Hash[String, String] + attr_reader ext: Hash[String, Hash[String, untyped]] def self.from_h: (Hash[untyped, untyped] hash) -> ClientConfigProfile @@ -52,13 +53,27 @@ module Temporalio ?namespace: String?, ?api_key: String?, ?tls: ClientConfigTLS?, - ?grpc_meta: Hash[String, String] + ?grpc_meta: Hash[String, String], + ?ext: Hash[String, Hash[String, untyped]] ) -> void def to_h: -> Hash[Symbol, untyped] def to_client_connect_options: -> [Array[untyped], Hash[Symbol, untyped]] end + module ClientConfigExtension + def name: -> String + + def client_plugin: (ClientConfigExtensionRequest request) -> Client::Plugin? + end + + class ClientConfigExtensionRequest + attr_reader profile: ClientConfigProfile + attr_reader env: Hash[String, String]? + + def initialize: (profile: ClientConfigProfile, env: Hash[String, String]?) -> void + end + class ClientConfig attr_reader profiles: Hash[String, ClientConfigProfile] @@ -76,9 +91,12 @@ module Temporalio ?disable_file: bool, ?disable_env: bool, ?config_file_strict: bool, - ?override_env_vars: Hash[String, String]? + ?override_env_vars: Hash[String, String]?, + ?extensions: Array[ClientConfigExtension] ) -> [Array[untyped], Hash[Symbol, untyped]] + def self._validate_extensions!: (Array[ClientConfigExtension] extensions) -> void + def initialize: (?profiles: Hash[String, ClientConfigProfile]) -> void def to_h: -> Hash[String, Hash[Symbol, untyped]] end diff --git a/temporalio/test/envconfig_test.rb b/temporalio/test/envconfig_test.rb index 133817bd..7c890463 100644 --- a/temporalio/test/envconfig_test.rb +++ b/temporalio/test/envconfig_test.rb @@ -63,6 +63,20 @@ class EnvConfigTest < Test "mixed_Case-header" = "mixed-value" TOML + # TOML config with extension tables + TOML_CONFIG_EXT = <<~TOML + [profile.default] + address = "default-address" + [profile.default.ext.first] + value = "first-value" + nested = { list = [1, "two"], flag = true } + [profile.default.ext.second.deeper] + value = 5 + + [profile.other] + address = "other-address" + TOML + def setup super @original_temporal_env = ENV.to_h.select { |key, _| key.start_with?('TEMPORAL_') } @@ -955,6 +969,305 @@ def test_e2e_multi_profile_different_client_connections refute_nil prod_client.connection.options.tls # TLS enabled with API key end + # ============================================================================= + # EXTENSION TESTS + # ============================================================================= + + # Extension that returns whatever the given block returns + class TestExtension + include Temporalio::EnvConfig::ClientConfigExtension + + attr_reader :name + + def initialize(name, &block) + @name = name + @block = block + end + + def client_plugin(request) + @block&.call(request) + end + end + + class TestPlugin + include Temporalio::Client::Plugin + + attr_reader :name + + def initialize(name) + @name = name + end + end + + def test_load_profile_with_ext + [false, true].each do |strict| + profile = Temporalio::EnvConfig::ClientConfigProfile.load(config_source: TOML_CONFIG_EXT, + config_file_strict: strict) + assert_equal 'default-address', profile.address + assert_equal( + { + 'first' => { 'value' => 'first-value', 'nested' => { 'list' => [1, 'two'], 'flag' => true } }, + 'second' => { 'deeper' => { 'value' => 5 } } + }, + profile.ext + ) + + config = Temporalio::EnvConfig::ClientConfig.load(config_source: TOML_CONFIG_EXT, config_file_strict: strict) + assert_equal profile.ext, config.profiles['default']&.ext + assert_empty config.profiles['other']&.ext + end + end + + def test_load_profile_ext_value_conversion + profile = Temporalio::EnvConfig::ClientConfigProfile.load(config_source: <<~TOML) + [profile.default.ext.my_ext] + float = 1.5 + datetime = 1979-05-27T07:32:00Z + local_datetime = 1979-05-27T07:32:00 + local_date = 1979-05-27 + local_time = 07:32:00 + nan = nan + inf = -inf + TOML + ext = profile.ext['my_ext'] + assert ext['nan'].nan? + assert_equal( + { + 'float' => 1.5, + 'datetime' => '1979-05-27T07:32:00Z', + 'local_datetime' => '1979-05-27T07:32:00', + 'local_date' => '1979-05-27', + 'local_time' => '07:32:00', + 'inf' => -Float::INFINITY + }, + ext.except('nan') + ) + end + + def test_strict_mode_rejects_unknown_non_ext_keys_alongside_ext + err = assert_raises(Temporalio::Internal::Bridge::Error) do + Temporalio::EnvConfig::ClientConfigProfile.load(config_source: <<~TOML, config_file_strict: true) + [profile.default] + unrecognized_field = "should-fail" + [profile.default.ext.first] + value = "first-value" + TOML + end + assert_includes err.message, 'unrecognized_field' + end + + def test_ext_ignores_env_vars + profile = Temporalio::EnvConfig::ClientConfigProfile.load( + config_source: "[profile.default.ext.first]\nvalue = \"file-value\"", + override_env_vars: { 'TEMPORAL_EXT_FIRST_VALUE' => 'env-value' } + ) + assert_equal({ 'first' => { 'value' => 'file-value' } }, profile.ext) + end + + def test_client_config_profile_ext_to_from_h + ext = { 'first' => { 'value' => 'first-value' } } + profile = Temporalio::EnvConfig::ClientConfigProfile.new(address: 'some-address', ext:) + assert_equal({ address: 'some-address', ext: }, profile.to_h) + assert_equal ext, Temporalio::EnvConfig::ClientConfigProfile.from_h(profile.to_h).ext + end + + def test_load_client_connect_options_calls_extensions_in_order + env = { 'TEMPORAL_ADDRESS' => 'env-address', 'TEMPORAL_EXT_FIRST_VALUE' => 'env-value' } + calls = [] + first = TestExtension.new('first') do |req| + calls << 'first' + # Env vars have already been applied to the profile + assert_equal 'env-address', req.profile.address + assert_equal({ 'value' => 'file-value' }, req.profile.ext['first']) + assert_same env, req.env + TestPlugin.new("first-#{req.env&.[]('TEMPORAL_EXT_FIRST_VALUE')}") + end + noop = TestExtension.new('noop') do |req| + calls << 'noop' + assert_nil req.profile.ext['noop'] + nil + end + second = TestExtension.new('second') do + calls << 'second' + TestPlugin.new('second') + end + + args, kwargs = Temporalio::EnvConfig::ClientConfig.load_client_connect_options( + config_source: <<~TOML, + [profile.default] + address = "file-address" + [profile.default.ext.first] + value = "file-value" + TOML + config_file_strict: true, + override_env_vars: env, + extensions: [second, first, noop] + ) + assert_equal %w[second first noop], calls + assert_equal(%w[second first-env-value], kwargs[:plugins].map(&:name)) + assert_equal 'env-address', args[0] + end + + def test_load_client_connect_options_omits_plugins_without_any + [[], [TestExtension.new('noop')]].each do |extensions| + _, kwargs = Temporalio::EnvConfig::ClientConfig.load_client_connect_options( + config_source: TOML_CONFIG_EXT, override_env_vars: {}, extensions: + ) + refute_includes kwargs, :plugins + end + end + + def test_load_client_connect_options_passes_env_to_extensions + got_envs = [] + ext = TestExtension.new('ext') do |req| + got_envs << req.env + nil + end + env = { 'TEMPORAL_EXT_EXT_VALUE' => 'value' } + + with_system_env_vars({ 'TEMPORAL_EXT_EXT_VALUE' => 'system-value' }) do + # Defaults to system env + Temporalio::EnvConfig::ClientConfig.load_client_connect_options(config_source: '', extensions: [ext]) + # Uses override env when given + Temporalio::EnvConfig::ClientConfig.load_client_connect_options( + config_source: '', override_env_vars: env, extensions: [ext] + ) + # nil when env is disabled + Temporalio::EnvConfig::ClientConfig.load_client_connect_options( + config_source: '', disable_env: true, override_env_vars: env, extensions: [ext] + ) + end + + assert_equal 3, got_envs.size + assert_equal 'system-value', got_envs[0]&.[]('TEMPORAL_EXT_EXT_VALUE') + assert_same env, got_envs[1] + assert_nil got_envs[2] + end + + def test_load_client_connect_options_rejects_invalid_extensions_before_calling_any + new_ext = ->(name) { TestExtension.new(name) { flunk 'extension should not be called' } } + [ + [[new_ext.call('a'), new_ext.call('b'), new_ext.call('a')], 'duplicate client config extension name "a"'], + [[new_ext.call('a'), new_ext.call('')], 'client config extension at index 1 has empty name'] + ].each do |extensions, expected_message| + err = assert_raises(ArgumentError) do + Temporalio::EnvConfig::ClientConfig.load_client_connect_options( + config_source: '', override_env_vars: {}, extensions: # steep:ignore + ) + end + assert_equal expected_message, err.message + end + + block = proc do + err = assert_raises(ArgumentError) do + Temporalio::EnvConfig::ClientConfig.load_client_connect_options( + config_source: '', override_env_vars: {}, extensions: [nil] # steep:ignore + ) + end + assert_equal 'NilClass does not implement EnvConfig::ClientConfigExtension', err.message + end + # Passing the wrong type is the point of this check, so the runtime signature check must not record it. + if defined?(SigApplicator) + SigApplicator.suppress_errors { block.call } + else + block.call + end + end + + def test_load_client_connect_options_wraps_extension_errors + ext_err = RuntimeError.new('bad config') + err = assert_raises(Temporalio::Error) do + Temporalio::EnvConfig::ClientConfig.load_client_connect_options( + config_source: '', override_env_vars: {}, extensions: [TestExtension.new('bad') { raise ext_err }] + ) + end + assert_equal 'client config extension "bad": bad config', err.message + assert_same ext_err, err.cause + end + + # Example extension that reads an API key from a file, configured like: + # + # [profile.default.ext.api_key_file] + # path = "/path/to/api-key" + # + # or with the TEMPORAL_EXT_API_KEY_FILE_PATH env var, which takes precedence. + class ApiKeyFileExtension + include Temporalio::EnvConfig::ClientConfigExtension + + def name + 'api_key_file' + end + + def client_plugin(request) + key_path = request.profile.ext.dig(name, 'path') + raise TypeError, "path must be a string, got #{key_path.class}" unless key_path.nil? || key_path.is_a?(String) + + key_path = request.env&.fetch('TEMPORAL_EXT_API_KEY_FILE_PATH', nil) || key_path + return nil if key_path.nil? + + Plugin.new(File.read(key_path).strip) + end + + class Plugin + include Temporalio::Client::Plugin + + def initialize(api_key) + @api_key = api_key + end + + def name + 'example.ApiKeyFileExtension' + end + + def configure_client(options) + options + end + + def connect_client(options, next_call) + next_call.call(options.with(api_key: @api_key, tls: options.tls || true)) + end + end + end + + def test_example_extension + Dir.mktmpdir do |tmpdir| # steep:ignore + key_path = File.join(tmpdir, 'api-key') + File.write(key_path, "my-api-key\n") + + args, kwargs = Temporalio::EnvConfig::ClientConfig.load_client_connect_options( + config_source: <<~TOML, + [profile.default] + address = "localhost:7233" + namespace = "my-namespace" + [profile.default.ext.api_key_file] + path = #{key_path.inspect} + TOML + config_file_strict: true, + override_env_vars: {}, + extensions: [ApiKeyFileExtension.new] + ) + client = Temporalio::Client.connect(*args, **kwargs, lazy_connect: true) # steep:ignore + assert_equal 'my-api-key', client.connection.options.api_key + assert client.connection.options.tls + + # Nothing configured + _, unconfigured_kwargs = Temporalio::EnvConfig::ClientConfig.load_client_connect_options( + config_source: '', override_env_vars: {}, extensions: [ApiKeyFileExtension.new] + ) + refute_includes unconfigured_kwargs, :plugins + + # Invalid config + err = assert_raises(Temporalio::Error) do + Temporalio::EnvConfig::ClientConfig.load_client_connect_options( + config_source: "[profile.default.ext.api_key_file]\npath = 5", + override_env_vars: {}, + extensions: [ApiKeyFileExtension.new] + ) + end + assert_equal 'client config extension "api_key_file": path must be a string, got Integer', err.message + end + end + private def with_temp_config_file(content) diff --git a/temporalio/test/sig/envconfig_test.rbs b/temporalio/test/sig/envconfig_test.rbs index 7a8a445b..1458e0df 100644 --- a/temporalio/test/sig/envconfig_test.rbs +++ b/temporalio/test/sig/envconfig_test.rbs @@ -4,9 +4,38 @@ class EnvConfigTest < Test TOML_CONFIG_MALFORMED: String TOML_CONFIG_TLS_DETAILED: String TOML_CONFIG_GRPC_META: String + TOML_CONFIG_EXT: String + + class TestExtension + include Temporalio::EnvConfig::ClientConfigExtension + + attr_reader name: String + + def initialize: ( + String name + ) ?{ (Temporalio::EnvConfig::ClientConfigExtensionRequest request) -> Temporalio::Client::Plugin? } -> void + end + + class TestPlugin + include Temporalio::Client::Plugin + + attr_reader name: String + + def initialize: (String name) -> void + end + + class ApiKeyFileExtension + include Temporalio::EnvConfig::ClientConfigExtension + + class Plugin + include Temporalio::Client::Plugin + + def initialize: (String api_key) -> void + end + end private def with_temp_config_file: [T] (String content) { (String config_file) -> T } -> T def with_system_env_vars: [T] (Hash[String, String] env_vars) { () -> T } -> T -end \ No newline at end of file +end