optify-config 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require 'sorbet-runtime'
5
+
6
+ module Optify
7
+ # @!visibility private
8
+ module ProviderModule
9
+ extend T::Sig
10
+
11
+ #: (Array[String] feature_names) -> Array[String]
12
+ def get_canonical_feature_names(feature_names)
13
+ # Try to optimize a typical case where there are just a few features.
14
+ # Ideally in production, a single feature that imports many other features is used for the most common scenarios.
15
+ # Benchmarks show that it is faster to use a loop than to call the Rust implementation which involves making a `Vec<String>` and returning a `Vec<String>`.
16
+ if feature_names.length < 4
17
+ feature_names.map { |feature_name| get_canonical_feature_name(feature_name) }
18
+ else
19
+ _get_canonical_feature_names(feature_names)
20
+ end
21
+ end
22
+
23
+ #: (String canonical_feature_name) -> Optify::OptionsMetadata?
24
+ def get_feature_metadata(canonical_feature_name)
25
+ metadata_json = get_feature_metadata_json(canonical_feature_name)
26
+ return nil if metadata_json.nil?
27
+
28
+ OptionsMetadata.from_hash(JSON.parse(metadata_json))
29
+ end
30
+
31
+ private
32
+
33
+ #: -> Hash[String, OptionsMetadata]
34
+ def _features_with_metadata
35
+ return @features_with_metadata if @features_with_metadata
36
+
37
+ result = JSON.parse(features_with_metadata_json)
38
+ result.each do |key, value|
39
+ result[key] = OptionsMetadata.from_hash(value)
40
+ end
41
+ result.freeze
42
+
43
+ @features_with_metadata = T.let(result, T.nilable(T::Hash[String, OptionsMetadata]))
44
+ result
45
+ end
46
+
47
+ # Fetches options based on the provided key and feature names.
48
+ #
49
+ # @param key The key to fetch options for.
50
+ # @param feature_names The enabled feature names to use to build the options.
51
+ # @param config_class The class of the configuration to return.
52
+ # The class must implement `from_hash` as a class method to convert a hash to an instance of the class.
53
+ # It is recommended to use a class that extends `Optify::BaseConfig` because it implements `from_hash`.
54
+ # @param cache_options Set this if caching is desired. Only very simple caching is supported for now.
55
+ # @param preferences The preferences to use when getting options.
56
+ # @return The options.
57
+ #: [Config] (String key, Array[String] feature_names, Class[Config] config_class, ?CacheOptions? cache_options, ?Optify::GetOptionsPreferences? preferences) -> Config
58
+ def _get_options(key, feature_names, config_class, cache_options = nil, preferences = nil)
59
+ return get_options_with_cache(key, feature_names, config_class, cache_options, preferences) if cache_options
60
+
61
+ unless config_class.respond_to?(:from_hash)
62
+ Kernel.raise NotImplementedError,
63
+ "The provided config class must implement `from_hash` as a class method
64
+ in order to be converted.
65
+ Recommended: extend `Optify::BaseConfig`."
66
+ end
67
+
68
+ options_json = if preferences
69
+ get_options_json_with_preferences(key, feature_names, preferences)
70
+ else
71
+ get_options_json(key, feature_names)
72
+ end
73
+ hash = JSON.parse(options_json)
74
+ T.unsafe(config_class).from_hash(hash)
75
+ end
76
+
77
+ #: -> void
78
+ def _init
79
+ @cache = T.let({}, T.nilable(T::Hash[T.untyped, T.untyped]))
80
+ @features_with_metadata = T.let(nil, T.nilable(T::Hash[String, OptionsMetadata]))
81
+ end
82
+
83
+ NOT_FOUND_IN_CACHE_SENTINEL = Object.new
84
+
85
+ #: [Config] (String key, Array[String] feature_names, Class[Config] config_class, Optify::CacheOptions _cache_options, ?Optify::GetOptionsPreferences? preferences) -> Config
86
+ def get_options_with_cache(key, feature_names, config_class, _cache_options, preferences = nil)
87
+ # Cache directly in Ruby instead of Rust because:
88
+ # * Avoid any possible conversion overhead.
89
+ # * Memory management: probably better to do it in Ruby for a Ruby app and avoid memory in Rust.
90
+ if preferences&.overrides?
91
+ Kernel.raise ArgumentError,
92
+ 'Caching when overrides are given is not supported. Do not pass cache options when using overrides in preferences.'
93
+ end
94
+
95
+ init unless @cache
96
+ feature_names = get_canonical_feature_names(feature_names) unless preferences&.skip_feature_name_conversion
97
+
98
+ cache_key = [key, feature_names, config_class]
99
+ result = @cache&.fetch(cache_key, NOT_FOUND_IN_CACHE_SENTINEL)
100
+ return result unless result.equal?(NOT_FOUND_IN_CACHE_SENTINEL)
101
+
102
+ # Handle a cache miss.
103
+
104
+ # We can avoid converting the features names because they're already converted, if that was desired.
105
+ if preferences.nil?
106
+ preferences = GetOptionsPreferences.new
107
+ preferences.skip_feature_name_conversion = true
108
+ else
109
+ # Indeed the copying of preferences could be wasteful, but this only happens on a cache miss
110
+ # and when no custom preferences are provided.
111
+ preferences = preferences.dup
112
+ preferences.skip_feature_name_conversion = true
113
+ end
114
+
115
+ result = get_options(key, feature_names, config_class, nil, preferences)
116
+
117
+ T.must(@cache)[cache_key] = result
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require 'json'
5
+
6
+ require 'sorbet-runtime'
7
+
8
+ require_relative './base_config'
9
+ require_relative './implementation'
10
+ require_relative './options_metadata'
11
+ require_relative './provider_module'
12
+
13
+ module Optify
14
+ # @!visibility private
15
+ class OptionsWatcher
16
+ include ProviderModule
17
+
18
+ # TODO: Find a better way to proxy the methods with copying the parameters.
19
+
20
+ #: -> Hash[String, OptionsMetadata]
21
+ def features_with_metadata
22
+ _check_cache
23
+ _features_with_metadata
24
+ end
25
+
26
+ #: [Config] (String key, Array[String] feature_names, Class[Config] config_class, ?CacheOptions? cache_options, ?Optify::GetOptionsPreferences? preferences) -> Config
27
+ def get_options(key, feature_names, config_class, cache_options = nil, preferences = nil)
28
+ _check_cache if cache_options
29
+
30
+ _get_options(key, feature_names, config_class, cache_options, preferences)
31
+ end
32
+
33
+ # (Optional) Eagerly initializes the cache.
34
+ # @return [OptionsWatcher] `self`.
35
+ #: -> OptionsWatcher
36
+ def init
37
+ _init
38
+ @cache_creation_time = T.let(Time.now, T.nilable(Time))
39
+ self
40
+ end
41
+
42
+ private
43
+
44
+ #: -> void
45
+ def _check_cache
46
+ return if @cache_creation_time && @cache_creation_time > last_modified
47
+
48
+ # The cache is not setup or it is out of date.
49
+ init
50
+ end
51
+ end
52
+ end
data/rbi/optify.rbi ADDED
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+ # typed: strong
3
+
4
+ # Tools for working with configurations declared in files.
5
+ module Optify
6
+ # A base class for classes from configuration files.
7
+ # Classes that derive from this can easily be used with `Optify::OptionsProvider.get_options`
8
+ # because they will have an implementation of `from_hash` that works recursively.
9
+ # This class is a work in progress with minimal error handling
10
+ # and doesn't handle certain cases such as nilable types yet.
11
+ # It may be moved to another gem in the future.
12
+ class BaseConfig
13
+ abstract!
14
+
15
+ # Create a new instance of the class from a hash.
16
+ #
17
+ # This is a class method that so that it can set members with private setters.
18
+ # @param hash The hash to create the instance from.
19
+ # @return The new instance.
20
+ sig { params(hash: T::Hash[T.untyped, T.untyped]).returns(T.attached_class) }
21
+ def self.from_hash(hash); end
22
+ end
23
+
24
+ # Options for caching.
25
+ # Only enabling or disabling caching is supported for now.
26
+ class CacheOptions < BaseConfig
27
+ end
28
+
29
+ # Information about a feature.
30
+ class OptionsMetadata < BaseConfig
31
+ sig { returns(T.nilable(T::Array[String])) }
32
+ def aliases; end
33
+
34
+ sig { returns(T.untyped) }
35
+ def details; end
36
+
37
+ sig { returns(String) }
38
+ def name; end
39
+
40
+ sig { returns(T.nilable(String)) }
41
+ def owners; end
42
+ end
43
+
44
+ # Preferences when getting options.
45
+ class GetOptionsPreferences
46
+ # Indicates if overrides are set.
47
+ sig { returns(T::Boolean) }
48
+ def overrides?; end
49
+
50
+ # Set overrides to apply after building the options based on the feature names.
51
+ # Do not provide overrides when requesting cached options.
52
+ # @param value The overrides to apply.
53
+ sig { params(value: T.nilable(T::Hash[T.untyped, T.untyped])).void }
54
+ def overrides=(value); end
55
+
56
+ # Set overrides to apply after building the options based on the feature names.
57
+ # Do not provide overrides when requesting cached options.
58
+ # @param value The overrides to apply as serialized JSON.
59
+ sig { params(value: T.nilable(String)).void }
60
+ def overrides_json=(value); end
61
+
62
+ sig { params(value: T::Boolean).void }
63
+ def skip_feature_name_conversion=(value); end
64
+
65
+ sig { returns(T::Boolean) }
66
+ def skip_feature_name_conversion; end
67
+ end
68
+
69
+ # A registry of features that provides configurations.
70
+ class OptionsRegistry
71
+ abstract!
72
+
73
+ # @return All of the canonical feature names.
74
+ sig { returns(T::Array[String]) }
75
+ def features; end
76
+
77
+ # @return All of the keys and values for the the features.
78
+ sig { returns(T::Hash[String, OptionsMetadata]) }
79
+ def features_with_metadata; end
80
+
81
+ # @return All of the keys and values for the the features.
82
+ sig do
83
+ params(feature_names: T::Array[String], preferences: GetOptionsPreferences)
84
+ .returns(String)
85
+ end
86
+ def get_all_options_json(feature_names, preferences); end
87
+ end
88
+
89
+ # A module only for internal use that provides the methods to help implement providers.
90
+ # Some of the methods shown within this module are implemented in Rust
91
+ # and are declared in this common module to avoid duplicate declarations in different classes.
92
+ module ProviderModule
93
+ # Map an alias or canonical feature name (perhaps derived from a file name) to a canonical feature name.
94
+ # Canonical feature names map to themselves.
95
+ #
96
+ # @param feature_name The name of an alias or a feature.
97
+ # @return The canonical feature name.
98
+ sig { params(feature_name: String).returns(String) }
99
+ def get_canonical_feature_name(feature_name); end
100
+
101
+ # Map aliases or canonical feature names (perhaps derived from a file names) to the canonical feature names.
102
+ # Canonical feature names map to themselves.
103
+ # This implementation may do an optimization for small arrays.
104
+ #
105
+ # @param feature_names The names of aliases or features.
106
+ # @return The canonical feature names.
107
+ sig { params(feature_names: T::Array[String]).returns(T::Array[String]) }
108
+ def get_canonical_feature_names(feature_names); end
109
+
110
+ # @return The metadata for the feature.
111
+ sig { params(canonical_feature_name: String).returns(T.nilable(OptionsMetadata)) }
112
+ def get_feature_metadata(canonical_feature_name); end
113
+
114
+ # Fetches options based on the provided key and feature names.
115
+ #
116
+ # @param key The key to fetch options for.
117
+ # @param feature_names The enabled feature names to use to build the options.
118
+ # @param config_class The class of the configuration to return.
119
+ # The class must implement `from_hash` as a class method to convert a hash to an instance of the class.
120
+ # It is recommended to use a class that extends `Optify::BaseConfig` because it implements `from_hash`.
121
+ # @param cache_options Set this if caching is desired. Only very simple caching is supported for now.
122
+ # @param preferences The preferences to use when getting options.
123
+ # @return The options.
124
+ sig do
125
+ type_parameters(:Config)
126
+ .params(
127
+ key: String,
128
+ feature_names: T::Array[String],
129
+ config_class: T::Class[T.type_parameter(:Config)],
130
+ cache_options: T.nilable(CacheOptions),
131
+ preferences: T.nilable(Optify::GetOptionsPreferences)
132
+ )
133
+ .returns(T.type_parameter(:Config))
134
+ end
135
+ def get_options(key, feature_names, config_class, cache_options = nil, preferences = nil); end
136
+
137
+ # Fetches options in JSON format based on the provided key and feature names.
138
+ #
139
+ # @param key [String] the key to fetch options for.
140
+ # @param feature_names [Array<String>] The enabled feature names to use to build the options.
141
+ # @return [String] the options in JSON.
142
+ sig { params(key: String, feature_names: T::Array[String]).returns(String) }
143
+ def get_options_json(key, feature_names); end
144
+
145
+ # Fetches options in JSON format based on the provided key and feature names.
146
+ #
147
+ # @param key [String] the key to fetch options for.
148
+ # @param feature_names [Array<String>] The enabled feature names to use to build the options.
149
+ # @param preferences [GetOptionsPreferences] The preferences to use when getting options.
150
+ # @return [String] the options in JSON.
151
+ sig do
152
+ params(key: String, feature_names: T::Array[String], preferences: GetOptionsPreferences)
153
+ .returns(String)
154
+ end
155
+ def get_options_json_with_preferences(key, feature_names, preferences); end
156
+
157
+ # (Optional) Eagerly initializes the cache.
158
+ # @return `self`.
159
+ sig { returns(T.self_type) }
160
+ def init; end
161
+
162
+ private
163
+
164
+ # Map aliases or canonical feature names (perhaps derived from a file names) to the canonical feature names.
165
+ # Canonical feature names map to themselves.
166
+ # This implementation calls the Rust implementation directly.
167
+ #
168
+ # @param feature_names The names of aliases or features.
169
+ # @return The canonical feature names.
170
+ sig { params(feature_names: T::Array[String]).returns(T::Array[String]) }
171
+ def _get_canonical_feature_names(feature_names); end
172
+
173
+ # @return The metadata for the feature.
174
+ sig { params(canonical_feature_name: String).returns(T.nilable(String)) }
175
+ def get_feature_metadata_json(canonical_feature_name); end
176
+
177
+ # @return All of the keys and values for the the features.
178
+ sig { returns(String) }
179
+ def features_with_metadata_json; end
180
+ end
181
+
182
+ # Provides configurations based on keys and enabled feature names.
183
+ class OptionsProvider < OptionsRegistry
184
+ include ProviderModule
185
+ end
186
+
187
+ # A builder for creating an `OptionsProvider` instance.
188
+ class OptionsProviderBuilder
189
+ # Adds a directory to the builder.
190
+ #
191
+ # @param path [String] The path of the directory to add.
192
+ # @return [OptionsProviderBuilder] `self`.
193
+ sig { params(path: String).returns(OptionsProviderBuilder) }
194
+ def add_directory(path); end
195
+
196
+ # @return [OptionsProvider] A newly built `OptionsProvider`.
197
+ sig { returns(OptionsProvider) }
198
+ def build; end
199
+ end
200
+
201
+ # Like `OptionsProvider` but also watches for changes to the files and reloads the options.
202
+ class OptionsWatcher < OptionsRegistry
203
+ include ProviderModule
204
+
205
+ # @return [Time] Returns the time when the provider was finished building.
206
+ sig { returns(Time) }
207
+ def last_modified; end
208
+ end
209
+
210
+ # A builder for creating an `OptionsWatcher` instance.
211
+ #
212
+ # This builder is kept separate from the `OptionsProviderBuilder`
213
+ # in order to keep `OptionsProviderBuilder` and `OptionsProvider` as simple and efficient as possible for production use.
214
+ class OptionsWatcherBuilder
215
+ # Adds a directory to watch for changes.
216
+ #
217
+ # @param path [String] The path of the directory to add.
218
+ # @return [OptionsWatcherBuilder] `self`.
219
+ sig { params(path: String).returns(OptionsWatcherBuilder) }
220
+ def add_directory(path); end
221
+
222
+ # @return [OptionsWatcher] A newly built `OptionsWatcher`.
223
+ sig { returns(OptionsWatcher) }
224
+ def build; end
225
+ end
226
+ end
data/sig/optify.rbs ADDED
@@ -0,0 +1,157 @@
1
+ # Tools for working with configurations declared in files.
2
+ module Optify
3
+ end
4
+
5
+ # A base class for classes from configuration files.
6
+ # Classes that derive from this can easily be used with `Optify::OptionsProvider.get_options`
7
+ # because they will have an implementation of `from_hash` that works recursively.
8
+ # This class is a work in progress with minimal error handling
9
+ # and doesn't handle certain cases such as nilable types yet.
10
+ # It may be moved to another gem in the future.
11
+ class Optify::BaseConfig
12
+ # Create a new instance of the class from a hash.
13
+ #
14
+ # This is a class method that so that it can set members with private setters.
15
+ # @param hash The hash to create the instance from.
16
+ # @return The new instance.
17
+ def self.from_hash: (::Hash[untyped, untyped] hash) -> instance
18
+ end
19
+
20
+ # Options for caching.
21
+ # Only enabling or disabling caching is supported for now.
22
+ class Optify::CacheOptions < BaseConfig
23
+ end
24
+
25
+ # Information about a feature.
26
+ class Optify::OptionsMetadata < BaseConfig
27
+ def aliases: () -> ::Array[String]?
28
+
29
+ def details: () -> untyped
30
+
31
+ def name: () -> String
32
+
33
+ def owners: () -> String?
34
+ end
35
+
36
+ # Preferences when getting options.
37
+ class Optify::GetOptionsPreferences
38
+ # Indicates if overrides are set.
39
+ def overrides?: () -> bool
40
+
41
+ # Set overrides to apply after building the options based on the feature names.
42
+ # Do not provide overrides when requesting cached options.
43
+ # @param value The overrides to apply.
44
+ def overrides=: (::Hash[untyped, untyped]? value) -> void
45
+
46
+ # Set overrides to apply after building the options based on the feature names.
47
+ # Do not provide overrides when requesting cached options.
48
+ # @param value The overrides to apply as serialized JSON.
49
+ def overrides_json=: (String? value) -> void
50
+
51
+ def skip_feature_name_conversion=: (bool value) -> void
52
+
53
+ def skip_feature_name_conversion: () -> bool
54
+ end
55
+
56
+ # A registry of features that provides configurations.
57
+ class Optify::OptionsRegistry
58
+ # @return All of the canonical feature names.
59
+ def features: () -> ::Array[String]
60
+
61
+ # @return All of the keys and values for the the features.
62
+ def features_with_metadata: () -> ::Hash[String, OptionsMetadata]
63
+
64
+ def get_all_options_json: (::Array[String] feature_names, GetOptionsPreferences preferences) -> String
65
+ end
66
+
67
+ # A module only for internal use that provides the methods to help implement providers.
68
+ # Some of the methods shown within this module are implemented in Rust
69
+ # and are declared in this common module to avoid duplicate declarations in different classes.
70
+ module Optify::ProviderModule
71
+ # Map an alias or canonical feature name (perhaps derived from a file name) to a canonical feature name.
72
+ # Canonical feature names map to themselves.
73
+ #
74
+ # @param feature_name The name of an alias or a feature.
75
+ # @return The canonical feature name.
76
+ def get_canonical_feature_name: (String feature_name) -> String
77
+
78
+ # Map aliases or canonical feature names (perhaps derived from a file names) to the canonical feature names.
79
+ # Canonical feature names map to themselves.
80
+ # This implementation may do an optimization for small arrays.
81
+ #
82
+ # @param feature_names The names of aliases or features.
83
+ # @return The canonical feature names.
84
+ def get_canonical_feature_names: (::Array[String] feature_names) -> ::Array[String]
85
+
86
+ # @return The metadata for the feature.
87
+ def get_feature_metadata: (String canonical_feature_name) -> OptionsMetadata?
88
+
89
+ def get_options: [Config] (String key, ::Array[String] feature_names, T::Class[Config] config_class, ?CacheOptions? cache_options, ?Optify::GetOptionsPreferences? preferences) -> Config
90
+
91
+ # Fetches options in JSON format based on the provided key and feature names.
92
+ #
93
+ # @param key [String] the key to fetch options for.
94
+ # @param feature_names [Array<String>] The enabled feature names to use to build the options.
95
+ # @return [String] the options in JSON.
96
+ def get_options_json: (String key, ::Array[String] feature_names) -> String
97
+
98
+ def get_options_json_with_preferences: (String key, ::Array[String] feature_names, GetOptionsPreferences preferences) -> String
99
+
100
+ # (Optional) Eagerly initializes the cache.
101
+ # @return `self`.
102
+ def init: () -> self
103
+
104
+ # Map aliases or canonical feature names (perhaps derived from a file names) to the canonical feature names.
105
+ # Canonical feature names map to themselves.
106
+ # This implementation calls the Rust implementation directly.
107
+ #
108
+ # @param feature_names The names of aliases or features.
109
+ # @return The canonical feature names.
110
+ def _get_canonical_feature_names: (::Array[String] feature_names) -> ::Array[String]
111
+
112
+ # @return The metadata for the feature.
113
+ def get_feature_metadata_json: (String canonical_feature_name) -> String?
114
+
115
+ # @return All of the keys and values for the the features.
116
+ def features_with_metadata_json: () -> String
117
+ end
118
+
119
+ # Provides configurations based on keys and enabled feature names.
120
+ class Optify::OptionsProvider < OptionsRegistry
121
+ include ProviderModule
122
+ end
123
+
124
+ # A builder for creating an `OptionsProvider` instance.
125
+ class Optify::OptionsProviderBuilder
126
+ # Adds a directory to the builder.
127
+ #
128
+ # @param path [String] The path of the directory to add.
129
+ # @return [OptionsProviderBuilder] `self`.
130
+ def add_directory: (String path) -> OptionsProviderBuilder
131
+
132
+ # @return [OptionsProvider] A newly built `OptionsProvider`.
133
+ def build: () -> OptionsProvider
134
+ end
135
+
136
+ # Like `OptionsProvider` but also watches for changes to the files and reloads the options.
137
+ class Optify::OptionsWatcher < OptionsRegistry
138
+ include ProviderModule
139
+
140
+ # @return [Time] Returns the time when the provider was finished building.
141
+ def last_modified: () -> Time
142
+ end
143
+
144
+ # A builder for creating an `OptionsWatcher` instance.
145
+ #
146
+ # This builder is kept separate from the `OptionsProviderBuilder`
147
+ # in order to keep `OptionsProviderBuilder` and `OptionsProvider` as simple and efficient as possible for production use.
148
+ class Optify::OptionsWatcherBuilder
149
+ # Adds a directory to watch for changes.
150
+ #
151
+ # @param path [String] The path of the directory to add.
152
+ # @return [OptionsWatcherBuilder] `self`.
153
+ def add_directory: (String path) -> OptionsWatcherBuilder
154
+
155
+ # @return [OptionsWatcher] A newly built `OptionsWatcher`.
156
+ def build: () -> OptionsWatcher
157
+ end