disposita 0.1.0
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 +7 -0
- data/CHANGELOG.md +35 -0
- data/LICENSE.txt +21 -0
- data/README.md +322 -0
- data/lib/disposita/configuration.rb +209 -0
- data/lib/disposita/error.rb +46 -0
- data/lib/disposita/formats/yaml.rb +67 -0
- data/lib/disposita/internal/deep_merge.rb +35 -0
- data/lib/disposita/internal/definition.rb +42 -0
- data/lib/disposita/internal/hash_tools.rb +100 -0
- data/lib/disposita/internal/schema_builder.rb +97 -0
- data/lib/disposita/internal/type_adapter.rb +70 -0
- data/lib/disposita/paths.rb +99 -0
- data/lib/disposita/schema.rb +312 -0
- data/lib/disposita/source.rb +79 -0
- data/lib/disposita/sources/environment.rb +55 -0
- data/lib/disposita/sources/file.rb +112 -0
- data/lib/disposita/sources/hash.rb +29 -0
- data/lib/disposita/types.rb +152 -0
- data/lib/disposita/version.rb +6 -0
- data/lib/disposita.rb +85 -0
- metadata +65 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Disposita
|
|
4
|
+
# Configuration-oriented type helpers used by schema declarations.
|
|
5
|
+
#
|
|
6
|
+
# Disposita intentionally does not implement a general-purpose runtime type
|
|
7
|
+
# system. Ruby classes such as +String+ and +Integer+ are accepted directly;
|
|
8
|
+
# these helpers cover configuration-specific shapes that Ruby does not express
|
|
9
|
+
# conveniently, such as booleans, finite enums and homogeneous arrays.
|
|
10
|
+
#
|
|
11
|
+
# Types participate in both validation and coercion. This matters especially
|
|
12
|
+
# for environment variables, where every input starts as a String.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# Disposita.define(:app) do
|
|
16
|
+
# setting :enabled, type: Disposita::Types.boolean, default: true
|
|
17
|
+
# setting :transport, type: Disposita::Types.enum(:ssh, :https)
|
|
18
|
+
# setting :hosts, type: Disposita::Types.array(String), default: []
|
|
19
|
+
# end
|
|
20
|
+
module Types
|
|
21
|
+
# Boolean configuration type supporting conventional textual forms.
|
|
22
|
+
#
|
|
23
|
+
# Resolved values are strictly +true+ or +false+. Coercion accepts common
|
|
24
|
+
# ENV-friendly strings such as +"true"+, +"yes"+, +"1"+, +"false"+,
|
|
25
|
+
# +"no"+ and +"0"+ case-insensitively.
|
|
26
|
+
class Boolean
|
|
27
|
+
# @param value [Object] candidate resolved value.
|
|
28
|
+
# @return [Boolean] whether +value+ is exactly +true+ or +false+.
|
|
29
|
+
def self.valid?(value) = [true, false].include?(value)
|
|
30
|
+
|
|
31
|
+
# Coerces conventional string representations to a Ruby boolean.
|
|
32
|
+
#
|
|
33
|
+
# @param value [Object] raw configuration value.
|
|
34
|
+
# @return [Boolean]
|
|
35
|
+
# @raise [ArgumentError] when no unambiguous boolean conversion exists.
|
|
36
|
+
def self.coerce(value)
|
|
37
|
+
return value if valid?(value)
|
|
38
|
+
return true if value.is_a?(String) && %w[true 1 yes on].include?(value.strip.downcase)
|
|
39
|
+
return false if value.is_a?(String) && %w[false 0 no off].include?(value.strip.downcase)
|
|
40
|
+
|
|
41
|
+
raise ArgumentError, "cannot coerce #{value.inspect} to boolean"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [String] concise name used in schema diagnostics.
|
|
45
|
+
def self.inspect = "Boolean"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Type representing a finite set of accepted configuration values.
|
|
49
|
+
#
|
|
50
|
+
# String inputs may be coerced to Symbol members, which makes enums useful
|
|
51
|
+
# for values arriving from ENV or YAML while preserving symbolic runtime
|
|
52
|
+
# APIs.
|
|
53
|
+
class Enum
|
|
54
|
+
# @return [Array<Object>] allowed values.
|
|
55
|
+
attr_reader :values
|
|
56
|
+
|
|
57
|
+
# @param values [Array<Object>] finite non-empty set of allowed values.
|
|
58
|
+
# @raise [ArgumentError] if the set is empty.
|
|
59
|
+
def initialize(values)
|
|
60
|
+
raise ArgumentError, "enum requires at least one value" if values.empty?
|
|
61
|
+
|
|
62
|
+
@values = values.freeze
|
|
63
|
+
freeze
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @param value [Object] candidate value.
|
|
67
|
+
# @return [Boolean] whether +value+ belongs to the enum.
|
|
68
|
+
def valid?(value) = values.include?(value)
|
|
69
|
+
|
|
70
|
+
# Coerces a String to a matching Symbol member when possible.
|
|
71
|
+
#
|
|
72
|
+
# @param value [Object] raw configuration value.
|
|
73
|
+
# @return [Object] matching enum member.
|
|
74
|
+
# @raise [ArgumentError] when +value+ cannot match any allowed member.
|
|
75
|
+
def coerce(value)
|
|
76
|
+
return value if valid?(value)
|
|
77
|
+
|
|
78
|
+
if value.is_a?(String)
|
|
79
|
+
symbol = value.to_sym
|
|
80
|
+
return symbol if values.include?(symbol)
|
|
81
|
+
return value if values.include?(value)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
raise ArgumentError, "expected one of #{values.map(&:inspect).join(', ')}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @return [String] human-readable representation used in diagnostics.
|
|
88
|
+
def inspect = "enum(#{values.map(&:inspect).join(', ')})"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Type representing an Array whose members share another declared type.
|
|
92
|
+
#
|
|
93
|
+
# Member types use the same adapter rules as top-level settings, so Ruby
|
|
94
|
+
# classes and other Disposita type helpers can be nested.
|
|
95
|
+
class ArrayOf
|
|
96
|
+
# @return [Object] declared member type.
|
|
97
|
+
attr_reader :member_type
|
|
98
|
+
|
|
99
|
+
# @param member_type [Object] type expected for every array member.
|
|
100
|
+
def initialize(member_type)
|
|
101
|
+
@member_type = member_type
|
|
102
|
+
freeze
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @param value [Object] candidate array.
|
|
106
|
+
# @return [Boolean] whether +value+ is an Array and every member is valid.
|
|
107
|
+
def valid?(value)
|
|
108
|
+
value.is_a?(Array) && value.all? { |member| Internal::TypeAdapter.valid?(member_type, member) }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Coerces every array member using the declared member type.
|
|
112
|
+
#
|
|
113
|
+
# @param value [Object] raw configuration value.
|
|
114
|
+
# @return [Array] coerced members.
|
|
115
|
+
# @raise [ArgumentError] if +value+ is not an Array or a member cannot be
|
|
116
|
+
# coerced.
|
|
117
|
+
def coerce(value)
|
|
118
|
+
raise ArgumentError, "expected Array" unless value.is_a?(Array)
|
|
119
|
+
|
|
120
|
+
value.map { |member| Internal::TypeAdapter.coerce(member_type, member) }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# @return [String] human-readable representation used in diagnostics.
|
|
124
|
+
def inspect = "Array[#{member_type.inspect}]"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
module_function
|
|
128
|
+
|
|
129
|
+
# Returns the built-in boolean type object.
|
|
130
|
+
#
|
|
131
|
+
# @return [Class<Disposita::Types::Boolean>]
|
|
132
|
+
def boolean = Boolean
|
|
133
|
+
|
|
134
|
+
# Builds a finite enum type.
|
|
135
|
+
#
|
|
136
|
+
# @param values [Array<Object>] accepted runtime values.
|
|
137
|
+
# @return [Disposita::Types::Enum]
|
|
138
|
+
# @raise [ArgumentError] when no values are supplied.
|
|
139
|
+
# @example
|
|
140
|
+
# type = Disposita::Types.enum(:ssh, :https)
|
|
141
|
+
# type.coerce("https") # => :https
|
|
142
|
+
def enum(*values) = Enum.new(values)
|
|
143
|
+
|
|
144
|
+
# Builds a homogeneous array type.
|
|
145
|
+
#
|
|
146
|
+
# @param member_type [Object] type required for every member.
|
|
147
|
+
# @return [Disposita::Types::ArrayOf]
|
|
148
|
+
# @example
|
|
149
|
+
# Disposita::Types.array(String)
|
|
150
|
+
def array(member_type) = ArrayOf.new(member_type)
|
|
151
|
+
end
|
|
152
|
+
end
|
data/lib/disposita.rb
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "disposita/version"
|
|
4
|
+
require_relative "disposita/error"
|
|
5
|
+
require_relative "disposita/internal/definition"
|
|
6
|
+
require_relative "disposita/internal/type_adapter"
|
|
7
|
+
require_relative "disposita/internal/schema_builder"
|
|
8
|
+
require_relative "disposita/internal/deep_merge"
|
|
9
|
+
require_relative "disposita/internal/hash_tools"
|
|
10
|
+
require_relative "disposita/types"
|
|
11
|
+
require_relative "disposita/source"
|
|
12
|
+
require_relative "disposita/formats/yaml"
|
|
13
|
+
require_relative "disposita/sources/hash"
|
|
14
|
+
require_relative "disposita/sources/environment"
|
|
15
|
+
require_relative "disposita/sources/file"
|
|
16
|
+
require_relative "disposita/paths"
|
|
17
|
+
require_relative "disposita/configuration"
|
|
18
|
+
require_relative "disposita/schema"
|
|
19
|
+
|
|
20
|
+
# Declarative, typed and layered configuration infrastructure for Ruby.
|
|
21
|
+
#
|
|
22
|
+
# Disposita lets a library or application own the meaning of its settings while
|
|
23
|
+
# delegating schema definition, coercion, validation, source precedence and
|
|
24
|
+
# persistence to a reusable configuration engine. Defining a schema does not
|
|
25
|
+
# read files, inspect ENV or mutate global state; consumers explicitly choose
|
|
26
|
+
# which sources participate when configuration is resolved.
|
|
27
|
+
#
|
|
28
|
+
# @example Define and resolve a small schema
|
|
29
|
+
# AppConfig = Disposita.define(:app, version: 1) do
|
|
30
|
+
# namespace :server do
|
|
31
|
+
# setting :host, type: String, default: "localhost"
|
|
32
|
+
# setting :port, type: Integer, default: 3000
|
|
33
|
+
# end
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# config = AppConfig.resolve([])
|
|
37
|
+
# config.server.host # => "localhost"
|
|
38
|
+
# config.server.port # => 3000
|
|
39
|
+
#
|
|
40
|
+
# @example Combine a file layer with environment overrides
|
|
41
|
+
# schema.load(
|
|
42
|
+
# sources: [Disposita::Sources::File.new(".app.yml", name: :project)],
|
|
43
|
+
# env: ENV,
|
|
44
|
+
# env_prefix: "APP"
|
|
45
|
+
# )
|
|
46
|
+
#
|
|
47
|
+
# @see Disposita::Schema
|
|
48
|
+
# @see Disposita::Configuration
|
|
49
|
+
# @see Disposita::Source
|
|
50
|
+
module Disposita
|
|
51
|
+
module_function
|
|
52
|
+
|
|
53
|
+
# Defines an independent configuration schema owned by the caller.
|
|
54
|
+
#
|
|
55
|
+
# The block is evaluated by Disposita's schema DSL. A schema is only a
|
|
56
|
+
# description until the caller explicitly resolves it against one or more
|
|
57
|
+
# sources, so this method is safe to use while loading a gem or application.
|
|
58
|
+
#
|
|
59
|
+
# @param name [String, Symbol] stable logical name of the consumer-owned
|
|
60
|
+
# configuration domain.
|
|
61
|
+
# @param version [Integer] version of the persisted schema format. Versioning
|
|
62
|
+
# belongs to the consumer and is independent from Disposita's gem version.
|
|
63
|
+
# @yield DSL used to declare namespaces and settings.
|
|
64
|
+
# @return [Disposita::Schema] immutable schema that can be resolved, inspected
|
|
65
|
+
# and used to validate writes.
|
|
66
|
+
# @raise [Disposita::SchemaError] if no block is supplied or the DSL contains
|
|
67
|
+
# an invalid schema definition.
|
|
68
|
+
#
|
|
69
|
+
# @example
|
|
70
|
+
# SCMConfig = Disposita.define(:scm, version: 1) do
|
|
71
|
+
# namespace :git do
|
|
72
|
+
# setting :remote, type: String, default: "origin"
|
|
73
|
+
# setting :transport,
|
|
74
|
+
# type: Disposita::Types.enum(:ssh, :https),
|
|
75
|
+
# default: :ssh
|
|
76
|
+
# end
|
|
77
|
+
# end
|
|
78
|
+
def define(name, version: 1, &block)
|
|
79
|
+
raise SchemaError, "a schema block is required" unless block
|
|
80
|
+
|
|
81
|
+
builder = Internal::SchemaBuilder.new
|
|
82
|
+
builder.instance_eval(&block)
|
|
83
|
+
Schema.new(name: name, version: version, settings: builder.settings)
|
|
84
|
+
end
|
|
85
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: disposita
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Rubcraft
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: |
|
|
13
|
+
Disposita provides consumer-owned configuration schemas, typed values,
|
|
14
|
+
coercion, validation, layered resolution, safe YAML persistence,
|
|
15
|
+
environment overrides, cross-platform paths and secret-aware diagnostics.
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- CHANGELOG.md
|
|
21
|
+
- LICENSE.txt
|
|
22
|
+
- README.md
|
|
23
|
+
- lib/disposita.rb
|
|
24
|
+
- lib/disposita/configuration.rb
|
|
25
|
+
- lib/disposita/error.rb
|
|
26
|
+
- lib/disposita/formats/yaml.rb
|
|
27
|
+
- lib/disposita/internal/deep_merge.rb
|
|
28
|
+
- lib/disposita/internal/definition.rb
|
|
29
|
+
- lib/disposita/internal/hash_tools.rb
|
|
30
|
+
- lib/disposita/internal/schema_builder.rb
|
|
31
|
+
- lib/disposita/internal/type_adapter.rb
|
|
32
|
+
- lib/disposita/paths.rb
|
|
33
|
+
- lib/disposita/schema.rb
|
|
34
|
+
- lib/disposita/source.rb
|
|
35
|
+
- lib/disposita/sources/environment.rb
|
|
36
|
+
- lib/disposita/sources/file.rb
|
|
37
|
+
- lib/disposita/sources/hash.rb
|
|
38
|
+
- lib/disposita/types.rb
|
|
39
|
+
- lib/disposita/version.rb
|
|
40
|
+
homepage: https://github.com/Rubcraft/disposita
|
|
41
|
+
licenses:
|
|
42
|
+
- MIT
|
|
43
|
+
metadata:
|
|
44
|
+
source_code_uri: https://github.com/Rubcraft/disposita
|
|
45
|
+
changelog_uri: https://github.com/Rubcraft/disposita/blob/main/CHANGELOG.md
|
|
46
|
+
bug_tracker_uri: https://github.com/Rubcraft/disposita/issues
|
|
47
|
+
rubygems_mfa_required: 'true'
|
|
48
|
+
rdoc_options: []
|
|
49
|
+
require_paths:
|
|
50
|
+
- lib
|
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: 3.2.0
|
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
requirements: []
|
|
62
|
+
rubygems_version: 4.0.16
|
|
63
|
+
specification_version: 4
|
|
64
|
+
summary: Declarative, typed and layered configuration for Ruby
|
|
65
|
+
test_files: []
|