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.
@@ -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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Disposita
4
+ # Current Disposita version.
5
+ VERSION = "0.1.0"
6
+ 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: []