strict 1.4.0 → 2.0.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 +4 -4
- data/.agents/resume +4 -0
- data/.agents/setup +105 -0
- data/.rspec +2 -0
- data/.rubocop.yml +14 -4
- data/.tool-versions +1 -1
- data/API.md +335 -0
- data/CHANGELOG.md +110 -1
- data/Gemfile +13 -0
- data/Gemfile.lock +118 -42
- data/README.md +278 -4
- data/Rakefile +13 -7
- data/benchmark/baseline.rb +204 -0
- data/lib/strict/assignment_error.rb +5 -2
- data/lib/strict/attribute.rb +8 -47
- data/lib/strict/attributes/class.rb +0 -6
- data/lib/strict/attributes/coercer.rb +4 -3
- data/lib/strict/attributes/configuration.rb +15 -0
- data/lib/strict/attributes/dsl.rb +18 -10
- data/lib/strict/attributes/generated_methods.rb +113 -0
- data/lib/strict/attributes/instance.rb +32 -15
- data/lib/strict/configuration.rb +44 -0
- data/lib/strict/declaration.rb +109 -0
- data/lib/strict/detailed_validator.rb +9 -0
- data/lib/strict/error.rb +8 -1
- data/lib/strict/initialization_error.rb +11 -2
- data/lib/strict/interface.rb +39 -21
- data/lib/strict/interfaces/conformance.rb +125 -0
- data/lib/strict/interfaces/instance.rb +1 -49
- data/lib/strict/method.rb +72 -53
- data/lib/strict/method_call_error.rb +12 -2
- data/lib/strict/method_return_error.rb +2 -2
- data/lib/strict/methods/dsl.rb +16 -8
- data/lib/strict/methods/module.rb +27 -8
- data/lib/strict/methods/verifiable_method.rb +234 -70
- data/lib/strict/object.rb +1 -1
- data/lib/strict/parameter.rb +3 -52
- data/lib/strict/return.rb +14 -10
- data/lib/strict/rspec.rb +159 -0
- data/lib/strict/union.rb +180 -0
- data/lib/strict/unions/coercer.rb +55 -0
- data/lib/strict/validation.rb +40 -0
- data/lib/strict/validators/all_of.rb +11 -3
- data/lib/strict/validators/array_of.rb +18 -3
- data/lib/strict/validators/hash_of.rb +23 -3
- data/lib/strict/value.rb +18 -4
- data/lib/strict/version.rb +1 -1
- data/lib/strict/violation.rb +9 -0
- data/lib/strict.rb +42 -0
- data/sig/strict/rspec.rbs +10 -0
- data/sig/strict.rbs +213 -1
- data/strict.gemspec +1 -9
- metadata +20 -120
- data/lib/strict/accessor/attributes.rb +0 -15
- data/lib/strict/accessor/module.rb +0 -45
- data/lib/strict/reader/attributes.rb +0 -15
- data/lib/strict/reader/module.rb +0 -27
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f7a4d9b33782e029a5bdaf97e300a83ccb21fc76629e92d9dbbf3d12c373eaa4
|
|
4
|
+
data.tar.gz: 715e362a7bb394c4f18d6eb46d828d5c5dbfe0090f85ed971a89369981254532
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 696e79a02adafa97c27575b5b3ac787aeb4923d07753d3149efbdf283a5357a0c74ccea46367cd6d0d8d49996e45e668418c203652738aa2a0efcc5ec24dbdf0
|
|
7
|
+
data.tar.gz: 751a0ebdc18322f456ebc834ec7a215dc28b0de66dc010be82bcb03a5cbfdfa4c160a66110d0b60e22c056cfe34d0827b2e13533ee580c1761987c570939a4be
|
data/.agents/resume
ADDED
data/.agents/setup
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
cd "$repo_root"
|
|
6
|
+
|
|
7
|
+
run_step() {
|
|
8
|
+
local description="$1"
|
|
9
|
+
shift
|
|
10
|
+
local started_at="$SECONDS"
|
|
11
|
+
|
|
12
|
+
printf '\n==> %s\n' "$description"
|
|
13
|
+
"$@"
|
|
14
|
+
printf '<== Finished in %ss\n' "$((SECONDS - started_at))"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
install_system_packages() {
|
|
18
|
+
local packages=(
|
|
19
|
+
autoconf
|
|
20
|
+
bison
|
|
21
|
+
build-essential
|
|
22
|
+
ca-certificates
|
|
23
|
+
curl
|
|
24
|
+
libdb-dev
|
|
25
|
+
libffi-dev
|
|
26
|
+
libgdbm-compat-dev
|
|
27
|
+
libgdbm-dev
|
|
28
|
+
libncurses-dev
|
|
29
|
+
libreadline-dev
|
|
30
|
+
libssl-dev
|
|
31
|
+
libyaml-dev
|
|
32
|
+
patch
|
|
33
|
+
uuid-dev
|
|
34
|
+
zlib1g-dev
|
|
35
|
+
)
|
|
36
|
+
local missing_packages=()
|
|
37
|
+
local package
|
|
38
|
+
|
|
39
|
+
for package in "${packages[@]}"; do
|
|
40
|
+
if ! dpkg-query -W -f='${Status}' "$package" 2>/dev/null | grep -Fq 'install ok installed'; then
|
|
41
|
+
missing_packages+=("$package")
|
|
42
|
+
fi
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
if ((${#missing_packages[@]} == 0)); then
|
|
46
|
+
printf 'All system packages are already installed.\n'
|
|
47
|
+
return
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
local apt=(apt-get)
|
|
51
|
+
if ((EUID != 0)); then
|
|
52
|
+
apt=(sudo apt-get)
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
"${apt[@]}" update
|
|
56
|
+
DEBIAN_FRONTEND=noninteractive "${apt[@]}" install -y "${missing_packages[@]}"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
install_mise() {
|
|
60
|
+
if [[ -x "$HOME/.local/bin/mise" ]]; then
|
|
61
|
+
printf 'mise is already installed.\n'
|
|
62
|
+
return
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
curl --fail --location --silent --show-error https://mise.run | sh
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
configure_login_shell() {
|
|
69
|
+
local profile="$HOME/.bash_profile"
|
|
70
|
+
local marker='# strict mise toolchain'
|
|
71
|
+
|
|
72
|
+
touch "$profile"
|
|
73
|
+
if grep -Fqx "$marker" "$profile"; then
|
|
74
|
+
printf 'The login-shell toolchain path is already configured.\n'
|
|
75
|
+
return
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
cat >> "$profile" <<'EOF'
|
|
79
|
+
|
|
80
|
+
# strict mise toolchain
|
|
81
|
+
case ":$PATH:" in
|
|
82
|
+
*":$HOME/.local/bin:"*) ;;
|
|
83
|
+
*) export PATH="$HOME/.local/bin:$PATH" ;;
|
|
84
|
+
esac
|
|
85
|
+
case ":$PATH:" in
|
|
86
|
+
*":$HOME/.local/share/mise/shims:"*) ;;
|
|
87
|
+
*) export PATH="$HOME/.local/share/mise/shims:$PATH" ;;
|
|
88
|
+
esac
|
|
89
|
+
EOF
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
run_step 'Install system packages' install_system_packages
|
|
93
|
+
run_step 'Install mise' install_mise
|
|
94
|
+
|
|
95
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
96
|
+
|
|
97
|
+
run_step 'Configure login-shell toolchain access' configure_login_shell
|
|
98
|
+
run_step 'Install the pinned Ruby toolchain' mise install
|
|
99
|
+
|
|
100
|
+
bundler_version="$(awk '/^BUNDLED WITH$/{getline; gsub(/^[[:space:]]+|[[:space:]]+$/, ""); print}' Gemfile.lock)"
|
|
101
|
+
if ! mise exec -- ruby -e 'exit Gem::Specification.find_all_by_name("bundler", ARGV.fetch(0)).empty? ? 1 : 0' "$bundler_version"; then
|
|
102
|
+
run_step "Install Bundler $bundler_version" mise exec -- gem install bundler --version "$bundler_version" --no-document
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
run_step 'Install gem dependencies' mise exec -- ./bin/setup
|
data/.rspec
ADDED
data/.rubocop.yml
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
- rubocop-
|
|
1
|
+
plugins:
|
|
2
|
+
- rubocop-factory_bot
|
|
3
3
|
- rubocop-rake
|
|
4
|
+
- rubocop-rspec
|
|
4
5
|
|
|
5
6
|
AllCops:
|
|
6
7
|
NewCops: enable
|
|
7
|
-
TargetRubyVersion: 3.
|
|
8
|
+
TargetRubyVersion: 3.3
|
|
8
9
|
|
|
9
10
|
Style/CaseEquality:
|
|
10
11
|
Enabled: false
|
|
@@ -15,7 +16,16 @@ Style/Documentation:
|
|
|
15
16
|
Style/StringLiterals:
|
|
16
17
|
EnforcedStyle: double_quotes
|
|
17
18
|
|
|
19
|
+
Style/SymbolProc:
|
|
20
|
+
Enabled: false
|
|
21
|
+
|
|
18
22
|
Metrics/BlockLength:
|
|
19
23
|
Exclude:
|
|
20
|
-
- "
|
|
24
|
+
- "spec/**/*"
|
|
21
25
|
- "*.gemspec"
|
|
26
|
+
|
|
27
|
+
RSpec/ExampleLength:
|
|
28
|
+
Enabled: false
|
|
29
|
+
|
|
30
|
+
RSpec/MultipleExpectations:
|
|
31
|
+
Enabled: false
|
data/.tool-versions
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
ruby 3.
|
|
1
|
+
ruby 3.3.12
|
data/API.md
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# Supported API
|
|
2
|
+
|
|
3
|
+
This document defines the compatibility boundary for Strict 2.0. Code inside this boundary is public API. Other reachable Ruby constants, methods, generated modules, and reflection details are implementation details unless this document identifies them as public.
|
|
4
|
+
|
|
5
|
+
## Capabilities
|
|
6
|
+
|
|
7
|
+
Bring each Strict capability into a class with `include`:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
include Strict::Value
|
|
11
|
+
include Strict::Object
|
|
12
|
+
include Strict::Method
|
|
13
|
+
include Strict::Interface
|
|
14
|
+
include Strict::Union
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The former `extend Strict::Method` and `extend Strict::Interface` forms are not part of this major-version API.
|
|
18
|
+
|
|
19
|
+
### Values and objects
|
|
20
|
+
|
|
21
|
+
`Strict::Value` and `Strict::Object` add an `attributes` declaration block. Each declaration accepts an attribute name, an optional validator, `coerce:`, and one of `default:`, `default_value:`, or `default_generator:`.
|
|
22
|
+
|
|
23
|
+
Attribute names must be strings or symbols in the supported identifier shape: a lowercase ASCII letter or underscore, followed by ASCII letters, digits, or underscores, with one optional trailing `?` or `!`. Use `strict_attribute` when a name is a Ruby reserved word or already resolves to a method inside the declaration block, for example `strict_attribute :if` or `strict_attribute :format`. Empty names, operators, setters, and other method-name forms are rejected. Generated accessors use the conventional backing instance variable formed by removing a trailing `?` or `!` from the attribute name, so `active?` uses `@active`. Class behavior can read this declared state directly; writing it directly bypasses validation and is unsupported. A mutable object's punctuation writer can be called with `public_send`, for example `object.public_send(:"active?=", false)`.
|
|
24
|
+
|
|
25
|
+
Both capabilities provide:
|
|
26
|
+
|
|
27
|
+
- keyword initialization that rejects missing, additional, or invalid attributes;
|
|
28
|
+
- declared readers;
|
|
29
|
+
- coercion before validation;
|
|
30
|
+
- `to_h`, with symbol keys in declaration order;
|
|
31
|
+
- a meaningful `inspect` and pretty-print representation, without a fixed formatting contract;
|
|
32
|
+
- class methods `strict_attributes` and `coercer`.
|
|
33
|
+
|
|
34
|
+
`Strict::Object` also provides validated writers. It retains Ruby's identity equality and does not provide `with`.
|
|
35
|
+
|
|
36
|
+
`Strict::Value` does not provide writers. It provides:
|
|
37
|
+
|
|
38
|
+
- `with(**attributes)`, which returns a validated instance of the same class;
|
|
39
|
+
- `deconstruct_keys(keys)`, for Ruby hash and class patterns;
|
|
40
|
+
- `==` and `eql?`, based on exact class and attribute values;
|
|
41
|
+
- `hash`, consistent with `eql?`.
|
|
42
|
+
|
|
43
|
+
A class can execute at most one `attributes` block, and an empty block counts as that one block. A subclass inherits its parent's attributes and can execute one `attributes` block without changing the parent. A declaration with the exact name of an inherited attribute replaces its validator, coercer, and default in the subclass while retaining its position in the attribute order. A declaration cannot duplicate another declaration in the same block. Attribute names also cannot map to the same backing instance variable, such as `active`, `active?`, and `active!`, including across inherited declarations.
|
|
44
|
+
|
|
45
|
+
Before it installs generated methods, Strict rejects an attribute whose reader would collide with a public, protected, or private instance method defined directly on the declaring class. It also rejects methods reserved by `BasicObject`, the active Strict capability, or Strict's generated implementation, including `class`, `to_h`, `inspect`, `hash`, `eql?`, `initialize`, `pretty_print`, and `public_send`. `Strict::Object` applies the same checks to its generated writer.
|
|
46
|
+
|
|
47
|
+
An attribute can override an inherited public, protected, or private method from a superclass or a non-Strict included module. The generated reader or writer is public and replaces the inherited behavior; it does not wrap or validate calls to the inherited method. Strict does not police methods defined after the `attributes` block; later overrides remain unsupported. Same-block duplicate attributes, repeated blocks, prohibited generated-method collisions, and invalid names or declaration options raise `ArgumentError` at declaration time.
|
|
48
|
+
|
|
49
|
+
#### Attribute introspection
|
|
50
|
+
|
|
51
|
+
`strict_attributes` is an ordered `Enumerable`. Each yielded descriptor supports:
|
|
52
|
+
|
|
53
|
+
- `name`
|
|
54
|
+
- `validator`
|
|
55
|
+
- `optional?`
|
|
56
|
+
|
|
57
|
+
The descriptor's concrete class and other methods are implementation details.
|
|
58
|
+
|
|
59
|
+
#### Defaults
|
|
60
|
+
|
|
61
|
+
- `default: value` uses the value directly.
|
|
62
|
+
- `default: callable` calls it for each default.
|
|
63
|
+
- `default_value: value` preserves the value even when it is callable.
|
|
64
|
+
- `default_generator: callable` calls it for each default and requires an object that responds to `call`.
|
|
65
|
+
|
|
66
|
+
Only one default option can be present on one declaration.
|
|
67
|
+
|
|
68
|
+
#### Attribute coercion
|
|
69
|
+
|
|
70
|
+
`coerce:` accepts:
|
|
71
|
+
|
|
72
|
+
- omission, which uses `validator.coercer` when available and otherwise disables coercion;
|
|
73
|
+
- `false`, which disables coercion inherited from the validator;
|
|
74
|
+
- a callable;
|
|
75
|
+
- a class method name as a symbol;
|
|
76
|
+
- `true`, which calls the class method `coerce_<attribute>`.
|
|
77
|
+
|
|
78
|
+
Other coercer forms raise `ArgumentError` at declaration time. Strict does not require a class-method coercer selected by a symbol or `true` to exist when the attribute is declared. The receiving class resolves it during initialization or assignment, so a subclass can provide or override it.
|
|
79
|
+
|
|
80
|
+
The generated class `coercer` returns `nil`, non-hash-like values, and instances of that exact class unchanged. A subclass instance is not an exact-class match: a parent class coercer treats it as hash-like input and creates a new parent instance from the parent's declared attributes. For other hash-like input, the coercer recognizes declared symbol or string keys and initializes the class with those values.
|
|
81
|
+
|
|
82
|
+
### Discriminated unions
|
|
83
|
+
|
|
84
|
+
`Strict::Union` defines a closed, discriminated union of generated value-object variants. A union must declare one discriminator before it declares variants:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
class PaymentResult
|
|
88
|
+
include Strict::Union
|
|
89
|
+
|
|
90
|
+
discriminator :status
|
|
91
|
+
|
|
92
|
+
attributes do
|
|
93
|
+
request_id String
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
variant :authorized, tag: "payment.authorized" do
|
|
97
|
+
attributes do
|
|
98
|
+
authorization_id String
|
|
99
|
+
amount_in_cents Integer
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def successful? = true
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
variant :declined do
|
|
106
|
+
attributes do
|
|
107
|
+
reason String
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def successful? = false
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
There is no default discriminator. A union can declare zero or one `attributes` block before its variants. Strict shares these attributes across every variant. The declaration can appear before or after the discriminator, but it cannot redeclare the discriminator.
|
|
116
|
+
|
|
117
|
+
A variant name must be a lower snake-case string or symbol, and Strict generates its PascalCase nested subclass. By default, the discriminator tag is the name's corresponding symbol. The optional `tag:` can assign a different string or symbol. For example, `variant :requires_action, tag: "action-required"` generates `PaymentResult::RequiresAction < PaymentResult` with the tag `"action-required"`.
|
|
118
|
+
|
|
119
|
+
The variant block configures the generated subclass. It can include modules, define methods, and contain zero or one `attributes` block. Strict combines the discriminator, shared union attributes, and variant attributes in that order. The discriminator is an implicit first attribute with a fixed default value:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
PaymentResult::Authorized.new(
|
|
123
|
+
request_id: "request_123",
|
|
124
|
+
authorization_id: "auth_123",
|
|
125
|
+
amount_in_cents: 1_000
|
|
126
|
+
).to_h
|
|
127
|
+
# => { status: "payment.authorized", request_id: "request_123", authorization_id: "auth_123", amount_in_cents: 1_000 }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Variants therefore use the documented `Strict::Value` behavior for initialization, validation, coercion, defaults, copying, equality, hashing, conversion, inspection, pattern matching, and declaration errors. A variant attribute cannot duplicate or share a backing instance variable with the discriminator or a shared union attribute, and generated readers cannot collide with methods defined in the variant block. The union base cannot be instantiated directly.
|
|
131
|
+
|
|
132
|
+
`PaymentResult === value` is true only when `value` has the exact class of a registered variant. Generated variant classes retain normal Ruby class matching, including matching instances of their subclasses. Union and variant inheritance are outside the compatibility boundary.
|
|
133
|
+
|
|
134
|
+
The union class `coercer`:
|
|
135
|
+
|
|
136
|
+
- returns `nil` and non-hash-like values unchanged;
|
|
137
|
+
- returns an existing exact union member unchanged;
|
|
138
|
+
- accepts the discriminator as a symbol or string key;
|
|
139
|
+
- accepts a registered tag as its equivalent symbol or string value and stores its configured form;
|
|
140
|
+
- dispatches hash-like input through the selected variant's value coercer;
|
|
141
|
+
- raises `ArgumentError` when the discriminator is missing or unknown.
|
|
142
|
+
|
|
143
|
+
A union class can be an attribute or method validator. Declarations use its coercer automatically, so they accept hash-like input unless coercion is disabled:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
payment_result PaymentResult
|
|
147
|
+
strict_payment_result PaymentResult, coerce: false
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Declaring a discriminator more than once, declaring equivalent duplicate string or symbol tags, using an invalid variant name or tag, replacing an existing generated constant, declaring multiple union or variant attribute blocks, declaring union attributes after a variant, or redeclaring the discriminator raises `ArgumentError`. Declaration return values, generated-class reflection details, and the exact text of declaration and coercion errors are outside the compatibility boundary.
|
|
151
|
+
|
|
152
|
+
### Signed methods
|
|
153
|
+
|
|
154
|
+
`Strict::Method` adds `sig`, which applies to the next instance or singleton method definition. A signature supports required and optional positional parameters, `*rest`, required and optional keyword parameters, `**keyrest`, and an optional `returns` declaration. Blocks pass through without validation.
|
|
155
|
+
|
|
156
|
+
Parameter declaration names must be supported strings or symbols in the identifier shape documented for attributes and must match the signed Ruby method's parameters. `strict_parameter` supports Ruby reserved words that cannot be expressed as ordinary calls in the DSL. Parameter declarations accept a validator, `coerce: false` or a callable coercer, and the three default forms described above. Other coercer forms raise `ArgumentError` at declaration time.
|
|
157
|
+
|
|
158
|
+
Strict verifies that signature names match the method definition. Calls coerce parameters, validate parameters, invoke the method, and validate its return value. `returns` is validate-only: the exact object produced by the method is the object returned to its caller. It does not accept `coerce:` or any other keyword and never transforms the returned value. A signature with no `returns` declaration accepts any return value.
|
|
159
|
+
|
|
160
|
+
A signature cannot declare the same parameter more than once or contain more than one `returns` declaration. Invalid names, invalid defaults or coercers, duplicate parameters, repeated returns, and unsupported return options raise `ArgumentError` when the signature is declared. Exact error messages are not fixed.
|
|
161
|
+
|
|
162
|
+
Inherited signed methods remain validated. A subclass override is unsigned unless the subclass provides a new signature.
|
|
163
|
+
|
|
164
|
+
The following behavior is outside the compatibility boundary:
|
|
165
|
+
|
|
166
|
+
- private or protected signed methods;
|
|
167
|
+
- repeated signatures or same-class method redefinition;
|
|
168
|
+
- DSL expression return values;
|
|
169
|
+
- generated wrapper owners, ancestors, parameters, source locations, or other reflection details.
|
|
170
|
+
|
|
171
|
+
### Interfaces
|
|
172
|
+
|
|
173
|
+
`Strict::Interface` adds `expose`, `.coercer`, `.implemented_by?`, and `.verify_implementation!`. `expose(name) { ... }` defines a validated, keyword-forwarding instance method.
|
|
174
|
+
|
|
175
|
+
Constructing an interface requires an implementation. The implementation must respond publicly to every exposed method and accept every keyword declared by that method. It can accept an interface keyword as a required or optional keyword, or through a keyword-rest parameter. Additional required positional or keyword parameters do not conform because they can reject a valid interface call. Additional optional positional or keyword parameters, positional-rest parameters, blocks, and ordinary implementation methods do not affect conformance.
|
|
176
|
+
|
|
177
|
+
`Interface.implemented_by?(adapter)` returns `true` or `false` without constructing an interface. `Interface.verify_implementation!(adapter)` performs the same check, returns `nil` when the adapter conforms, and otherwise raises `Strict::ImplementationDoesNotConformError` with the structured details described below. Interface construction uses this same check.
|
|
178
|
+
|
|
179
|
+
Interface instances expose their `implementation`. The class coercer:
|
|
180
|
+
|
|
181
|
+
- returns `nil` unchanged;
|
|
182
|
+
- returns an instance of that exact interface unchanged;
|
|
183
|
+
- otherwise wraps the value and checks conformance.
|
|
184
|
+
|
|
185
|
+
When an interface class is an attribute or parameter validator, declarations use this coercer automatically.
|
|
186
|
+
|
|
187
|
+
Interface subclassing and re-exposing a method are outside the compatibility boundary.
|
|
188
|
+
|
|
189
|
+
## RSpec integration
|
|
190
|
+
|
|
191
|
+
Strict provides opt-in integration with RSpec 3.13. RSpec is not a runtime dependency and `require "strict"` does not
|
|
192
|
+
load it. Applications that use the integration must add RSpec to their test bundle and load the adapter from their spec
|
|
193
|
+
helper:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
require "strict/rspec"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Loading the adapter registers the `validate` and `conform_to` matchers and adds `strict_double` to RSpec example groups.
|
|
200
|
+
|
|
201
|
+
### RSpec matchers
|
|
202
|
+
|
|
203
|
+
`expect(validator).to validate(value)` applies the ordinary `===` validator protocol. For a
|
|
204
|
+
`Strict::DetailedValidator`, it uses `violations(value)` and includes root or nested violation paths when the expectation
|
|
205
|
+
fails. RSpec matcher objects and compatible instance doubles are accepted as test values. Exact failure-message wording
|
|
206
|
+
and formatting are not fixed.
|
|
207
|
+
|
|
208
|
+
`expect(implementation).to conform_to(interface)` uses `interface.verify_implementation!` to check the same conformance as
|
|
209
|
+
constructing the interface. It does not invoke exposed methods. A failed expectation includes the conformance error's
|
|
210
|
+
details, without a fixed wording or formatting contract.
|
|
211
|
+
|
|
212
|
+
### RSpec doubles and matcher placeholders
|
|
213
|
+
|
|
214
|
+
`strict_double(strict_class, stubs = {})` returns an RSpec instance verifying double. For a `Strict::Interface`, every
|
|
215
|
+
exposed method is stubbed to return `nil` unless `stubs` provides another result. Calls through the interface still
|
|
216
|
+
validate parameters and return values, so an omitted stub can cause `Strict::MethodReturnError` when `nil` is not a valid
|
|
217
|
+
return. For other classes, only the supplied methods are stubbed. RSpec rejects stubs for methods that the doubled class
|
|
218
|
+
does not expose.
|
|
219
|
+
|
|
220
|
+
RSpec instance verifying doubles created by `strict_double` or `instance_double` satisfy a class or module validator
|
|
221
|
+
when an instance of the doubled class would satisfy it. This applies to attributes, signed parameters, return values,
|
|
222
|
+
and values nested inside `ArrayOf`, `HashOf`, or `AllOf`. Plain, non-verifying doubles do not bypass validation.
|
|
223
|
+
|
|
224
|
+
RSpec matcher objects can stand in for validated test values at those same locations. When an expected `Strict::Value`
|
|
225
|
+
is used in an RSpec argument expectation, RSpec recursively applies matcher objects in its attributes. The expected and
|
|
226
|
+
actual values must have the same exact class. Loading the adapter does not change `Strict::Value#==`, `#eql?`, or `#hash`.
|
|
227
|
+
|
|
228
|
+
`Strict::RSpec` is the public integration namespace. Its nested constants, singleton methods, generated RSpec matcher
|
|
229
|
+
classes, RSpec proxy metadata, and reflection details are outside the compatibility boundary.
|
|
230
|
+
|
|
231
|
+
## Validators and coercers
|
|
232
|
+
|
|
233
|
+
Any object that implements `===` can be a validator. This includes classes, modules, literals, and custom validators. When a validator rejects a value, Strict reports that validator in one `:invalid` violation at the current path.
|
|
234
|
+
|
|
235
|
+
Attribute and parameter declarations automatically use `validator.coercer` when the validator responds to `coercer`. An explicit `coerce:` value takes precedence, and `coerce: false` disables this automatic coercion.
|
|
236
|
+
|
|
237
|
+
A custom validator can opt into nested structured failures by including `Strict::DetailedValidator` and implementing `violations(value)`. The method must return an array of `Strict::Violation` records and return an empty array when the value is valid. Each violation path is relative to the validated value; Strict prefixes paths when the validator is nested inside a declaration or a built-in detailed validator. `Strict::DetailedValidator` implements `===` from `violations`, so the custom validator does not need to implement both methods.
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
class Emails
|
|
241
|
+
include Strict::DetailedValidator
|
|
242
|
+
|
|
243
|
+
def violations(value)
|
|
244
|
+
unless Array === value
|
|
245
|
+
return [Strict::Violation.new(path: [], code: :invalid, value: value, validator: Array)]
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
value.each_with_index.filter_map do |email, index|
|
|
249
|
+
next if String === email
|
|
250
|
+
|
|
251
|
+
Strict::Violation.new(path: [index], code: :invalid, value: email, validator: String)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`Strict::Violation.new` accepts the four keyword arguments exposed by its readers: `path:`, `code:`, `value:`, and `validator:`. Validators that only implement `===` remain fully supported and do not need to include `Strict::DetailedValidator`.
|
|
258
|
+
|
|
259
|
+
Attribute and method DSL blocks provide these validator constructors:
|
|
260
|
+
|
|
261
|
+
- `AllOf(*validators)`
|
|
262
|
+
- `AnyOf(*validators)`
|
|
263
|
+
- `Anything()`
|
|
264
|
+
- `ArrayOf(element_validator)`
|
|
265
|
+
- `Boolean()`
|
|
266
|
+
- `HashOf(key_validator => value_validator)`
|
|
267
|
+
- `RangeOf(element_validator)`
|
|
268
|
+
|
|
269
|
+
They also provide these coercer constructors:
|
|
270
|
+
|
|
271
|
+
- `ToArray(with: nil)`
|
|
272
|
+
- `ToHash(with_keys: nil, with_values: nil)`
|
|
273
|
+
|
|
274
|
+
`ArrayOf` provides a coercer with the same array-like conversion as `ToArray` and uses its element validator's coercer when available. `HashOf` provides the corresponding `ToHash` behavior and uses its key and value validators' coercers when available. This coercion composes recursively, such as for `ArrayOf(ValueClass)` or `HashOf(String => ArrayOf(ValueClass))`.
|
|
275
|
+
|
|
276
|
+
The other built-in validators do not provide coercers. Their conversions have no single safe default: logical validators cannot unambiguously select or order coercers, ranges have no supported conversion contract, and booleans have no canonical input conversion.
|
|
277
|
+
|
|
278
|
+
The constructors' validation and conversion results are public. Their concrete `Strict::Validators::*` and `Strict::Coercers::*` classes, metadata readers, and exact string representations are not public API.
|
|
279
|
+
|
|
280
|
+
## Configuration
|
|
281
|
+
|
|
282
|
+
Strict provides:
|
|
283
|
+
|
|
284
|
+
- `Strict.configuration`
|
|
285
|
+
- `Strict.configure { |configuration| ... }`
|
|
286
|
+
- `Strict.with_overrides(**options) { ... }`
|
|
287
|
+
|
|
288
|
+
The supported configuration options are `random` and `sample_rate`, with readers and writers. `random` must implement `Random::Formatter`. `sample_rate` must be in the inclusive range from `0` through `1`.
|
|
289
|
+
|
|
290
|
+
Overrides are local to the current execution context (fiber), can be nested, and are restored when a block returns or raises. Neither a newly created fiber nor a new thread inherits an active override. An active override cannot be changed with `Strict.configure`.
|
|
291
|
+
|
|
292
|
+
Sampling controls validator calls. Attribute and parameter coercion, signature-definition checks, and interface conformance checks still run when validation is sampled out. Return validation can be sampled out, but return coercion never occurs.
|
|
293
|
+
|
|
294
|
+
Direct construction of `Strict::Configuration`, its `validate?` and `to_h` methods, block return values, and configuration object identity are outside the compatibility boundary.
|
|
295
|
+
|
|
296
|
+
## Exceptions
|
|
297
|
+
|
|
298
|
+
These exception classes are public and inherit from `Strict::Error`:
|
|
299
|
+
|
|
300
|
+
- `Strict::AssignmentError`
|
|
301
|
+
- `Strict::InitializationError`
|
|
302
|
+
- `Strict::ImplementationDoesNotConformError`
|
|
303
|
+
- `Strict::MethodCallError`
|
|
304
|
+
- `Strict::MethodDefinitionError`
|
|
305
|
+
- `Strict::MethodReturnError`
|
|
306
|
+
|
|
307
|
+
Messages identify the failure but their exact wording and formatting are not fixed. Exception constructor signatures are internal.
|
|
308
|
+
|
|
309
|
+
Every `Strict::Error` provides `#violations`, which returns an array of `Strict::Violation` records. Assignment, initialization, method-call, and method-return errors report runtime validation and structural input failures. Other errors return an empty array.
|
|
310
|
+
|
|
311
|
+
Each violation provides:
|
|
312
|
+
|
|
313
|
+
- `path`: the location of the failure;
|
|
314
|
+
- `code`: `:invalid`, `:missing`, or `:unexpected`;
|
|
315
|
+
- `value`: the rejected or unexpected value, or `nil` for a missing value;
|
|
316
|
+
- `validator`: the validator that rejected or required the value, or `nil` for an unexpected value.
|
|
317
|
+
|
|
318
|
+
An attribute or parameter name is the first path segment. Array elements use zero-based indices, and hash entries use their actual keys. Unexpected positional arguments use their zero-based argument indices. Return values start at the root, so a simple invalid return has an empty path and a nested return starts with its collection segment. `ArrayOf`, `HashOf`, and `AllOf` preserve nested failure paths. Failure order is not fixed.
|
|
319
|
+
|
|
320
|
+
The supported readers are:
|
|
321
|
+
|
|
322
|
+
- `AssignmentError#value`
|
|
323
|
+
- `InitializationError#remaining_attributes` and `#missing_attributes`
|
|
324
|
+
- `ImplementationDoesNotConformError#interface`, `#receiver`, `#missing_methods`, and `#invalid_method_definitions`
|
|
325
|
+
- `MethodCallError#remaining_args`, `#remaining_kwargs`, and `#missing_parameters`
|
|
326
|
+
- `MethodDefinitionError#missing_parameters` and `#additional_parameters`
|
|
327
|
+
- `MethodReturnError#value`
|
|
328
|
+
|
|
329
|
+
Readers that expose attribute, parameter, or method descriptors are internal.
|
|
330
|
+
|
|
331
|
+
## Constants and implementation details
|
|
332
|
+
|
|
333
|
+
`Strict::VERSION`, `Strict::Violation`, and `Strict::DetailedValidator` are public. `Strict::ISSUE_TRACKER` is internal.
|
|
334
|
+
|
|
335
|
+
`Strict::Attribute`, `Strict::Parameter`, `Strict::Return`, `strict_class_methods`, and `strict_instance_methods` are internal. The `Strict::Accessor`, `Strict::Reader`, `Strict::Attributes`, `Strict::Dsl`, `Strict::Interfaces`, `Strict::Methods`, and `Strict::Unions` namespaces and their contents are also internal.
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,114 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [2.0.0] - 2026-08-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Define and characterize the supported 2.0 API in `API.md`, provide RBS signatures for that boundary, validate them in the default checks, and add repeatable timing and allocation benchmark tooling.
|
|
8
|
+
- Add closed discriminated unions whose generated variants use `Strict::Value` for attributes and value behavior.
|
|
9
|
+
- Add inherited value and object attribute declarations and shared union attributes.
|
|
10
|
+
- Add structured validation violations with paths, codes, rejected values, and validators for assignment, initialization, signed method calls, and returns, plus an optional detailed-validator protocol for custom nested failures.
|
|
11
|
+
- Add opt-in RSpec matchers, verifying doubles, and nested matcher placeholders for Strict values and validations.
|
|
12
|
+
- Add `implemented_by?` and `verify_implementation!` to check interface adapters without constructing an interface.
|
|
13
|
+
|
|
14
|
+
### Breaking changes
|
|
15
|
+
|
|
16
|
+
- Standardize all four capabilities on `include`: `Strict::Value`, `Strict::Object`, `Strict::Method`, and `Strict::Interface`. The legacy `extend Strict::Method` and `extend Strict::Interface` forms are outside the supported 2.0 API.
|
|
17
|
+
- Make signed method returns validate-only. `returns` rejects coercion options, validates the original result, and preserves its identity for the caller.
|
|
18
|
+
- Reject malformed or same-block duplicate attribute declarations, repeated attribute blocks, duplicate or repeated signatures, and same-class or Strict/core-reserved generated attribute method collisions with `ArgumentError` instead of silently replacing declarations or installing pathological methods.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Require Ruby 3.3 or newer and test against all maintained Ruby releases.
|
|
23
|
+
- Update development dependencies to their latest release lines while retaining Zeitwerk 2.x compatibility from 2.6 onward.
|
|
24
|
+
- Compile signed-method invocation metadata once, reuse unchanged call arguments, and consolidate generated wrappers by owner.
|
|
25
|
+
- Preserve validated explicit keywords when keyrest processing changes a signed call.
|
|
26
|
+
- Forward validated interface calls directly to implementations and compile interface-conformance expectations once per interface definition.
|
|
27
|
+
- Check interface implementation signatures by substitutability: require every exposed keyword and reject only additional parameters that can constrain a valid interface call.
|
|
28
|
+
- Reduce value and object hot-path allocations during initialization, `to_h`, equality, and hashing.
|
|
29
|
+
- Share declaration invariants between attributes and parameters, preserve conventional attribute backing instance variables, and reject attribute names that map to the same storage.
|
|
30
|
+
- Automatically use a validator's coercer for attributes and parameters unless the declaration overrides or disables coercion.
|
|
31
|
+
- Propagate default coercion through `ArrayOf` elements and `HashOf` keys and values.
|
|
32
|
+
- Keep Strict configuration overrides in isolated internal execution-context storage to avoid collisions with application keys.
|
|
33
|
+
- Generate attribute readers and writers through one shared implementation, with mutable writers bound directly to their declarations.
|
|
34
|
+
- Allow generated attribute readers and writers to override inherited public, protected, and private methods while continuing to reject same-class and Strict/core-reserved collisions.
|
|
35
|
+
- Allow value and object subclasses to redefine inherited attributes without changing the parent or attribute order.
|
|
36
|
+
- Return exact `Strict::Value` and `Strict::Object` instances unchanged from their class coercers while continuing to convert subclass instances.
|
|
37
|
+
- Validate declaration names, explicit default generators, and capability-specific coercer forms before installing attributes or signatures.
|
|
38
|
+
|
|
39
|
+
### Performance
|
|
40
|
+
|
|
41
|
+
Representative microbenchmarks compared `90ba08a` (before) with `daa388d` (after) on Ruby 3.3.12, with YJIT disabled. Both revisions used the same pinned harness and public workloads. Results are the median of six interleaved process-run medians; each run used 50,000 warmup iterations, 200,000 measured iterations, and nine timing samples. Allocation counts were stable across all six runs.
|
|
42
|
+
|
|
43
|
+
| Operation | `90ba08a` ns/op | `daa388d` ns/op | Change | Allocations/op |
|
|
44
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
45
|
+
| Value initialization | 4,339 | 2,017 | -54% | 11 → 4 |
|
|
46
|
+
| Mutable assignment | 456 | 431 | -5% | 0 → 0 |
|
|
47
|
+
| Verified method call | 2,118 | 1,981 | -6% | 10 → 6 |
|
|
48
|
+
| Interface construction | 5,054 | 1,145 | -77% | 19 → 4 |
|
|
49
|
+
| Interface call | 3,230 | 2,340 | -28% | 16 → 8 |
|
|
50
|
+
| `to_h` | 1,301 | 852 | -35% | 6 → 2 |
|
|
51
|
+
| Equality | 2,786 | 1,433 | -49% | 12 → 3 |
|
|
52
|
+
| Hashing | 1,847 | 926 | -50% | 6 → 2 |
|
|
53
|
+
|
|
54
|
+
The equal-weight geometric mean of the eight after/before timing ratios is 0.57, or 43% lower. These microbenchmarks are environment-dependent measurements, not performance guarantees.
|
|
55
|
+
|
|
56
|
+
## [1.5.0] - 2023-04-12
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
|
|
60
|
+
- Add global configuration through `Strict.configure` and nested, thread-local overrides through `Strict.with_overrides`.
|
|
61
|
+
- Add configurable validation sampling for attributes, parameters, and return values through `sample_rate` and `random`.
|
|
62
|
+
|
|
63
|
+
## [1.4.0] - 2022-11-02
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- Allow interface implementations to use `*args` and `**kwargs` for parameters they do not declare directly, while continuing to validate calls made through the interface.
|
|
68
|
+
|
|
69
|
+
## [1.3.1] - 2022-10-20
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
|
|
73
|
+
- Support parameterless methods in `Strict::Interface` without generating invalid method syntax.
|
|
74
|
+
|
|
75
|
+
## [1.3.0] - 2022-10-18
|
|
76
|
+
|
|
77
|
+
### Added
|
|
78
|
+
|
|
79
|
+
- Add `.coercer` to classes that extend `Strict::Interface` so they can validate and wrap implementations through the coercion protocol.
|
|
80
|
+
|
|
81
|
+
## [1.2.0] - 2022-10-14
|
|
82
|
+
|
|
83
|
+
### Changed
|
|
84
|
+
|
|
85
|
+
- Support Ruby 3.0 and newer instead of requiring Ruby 3.1 or newer.
|
|
86
|
+
|
|
87
|
+
## [1.1.0] - 2022-10-14
|
|
88
|
+
|
|
89
|
+
### Added
|
|
90
|
+
|
|
91
|
+
- Add `Strict::Interface` for defining validated interfaces and checking that implementations conform to their exposed methods.
|
|
92
|
+
- Add coercers for `Strict::Value` and `Strict::Object` classes and for array and hash values.
|
|
93
|
+
|
|
94
|
+
## [1.0.0] - 2022-10-12
|
|
95
|
+
|
|
96
|
+
### Added
|
|
97
|
+
|
|
98
|
+
- Publish the initial Strict release with immutable `Strict::Value` classes, mutable `Strict::Object` classes, and runtime method-call validation through `Strict::Method`.
|
|
99
|
+
- Add the attribute and method-signature DSLs, built-in validators, coercion, structured errors, value equality and cloning, and object assignment validation.
|
|
100
|
+
|
|
3
101
|
## [0.0.0] - 2022-10-01
|
|
4
102
|
|
|
5
|
-
-
|
|
103
|
+
- Start initial development.
|
|
104
|
+
|
|
105
|
+
[Unreleased]: https://github.com/kylekthompson/strict/compare/v2.0.0...HEAD
|
|
106
|
+
[2.0.0]: https://github.com/kylekthompson/strict/compare/v1.5.0...v2.0.0
|
|
107
|
+
[1.5.0]: https://github.com/kylekthompson/strict/compare/v1.4.0...v1.5.0
|
|
108
|
+
[1.4.0]: https://github.com/kylekthompson/strict/compare/v1.3.1...v1.4.0
|
|
109
|
+
[1.3.1]: https://github.com/kylekthompson/strict/compare/v1.3.0...v1.3.1
|
|
110
|
+
[1.3.0]: https://github.com/kylekthompson/strict/compare/v1.2.0...v1.3.0
|
|
111
|
+
[1.2.0]: https://github.com/kylekthompson/strict/compare/v1.1.0...v1.2.0
|
|
112
|
+
[1.1.0]: https://github.com/kylekthompson/strict/compare/v1.0.0...v1.1.0
|
|
113
|
+
[1.0.0]: https://github.com/kylekthompson/strict/releases/tag/v1.0.0
|
|
114
|
+
[0.0.0]: https://github.com/kylekthompson/strict/commit/27fbe42e0d0d3c5ce86493e1252cf889f5a746d9
|
data/Gemfile
CHANGED
|
@@ -4,3 +4,16 @@ source "https://rubygems.org"
|
|
|
4
4
|
|
|
5
5
|
# Specify your gem's dependencies in strict.gemspec
|
|
6
6
|
gemspec
|
|
7
|
+
|
|
8
|
+
group :development do
|
|
9
|
+
gem "debug", "~> 1.11"
|
|
10
|
+
gem "factory_bot", "~> 6.5"
|
|
11
|
+
gem "gem-release", "~> 2.2"
|
|
12
|
+
gem "rake", "~> 13.4"
|
|
13
|
+
gem "rbs", "~> 4.1"
|
|
14
|
+
gem "rspec", "~> 3.13"
|
|
15
|
+
gem "rubocop", "~> 1.89"
|
|
16
|
+
gem "rubocop-factory_bot", "~> 2.28"
|
|
17
|
+
gem "rubocop-rake", "~> 0.7"
|
|
18
|
+
gem "rubocop-rspec", "~> 3.10"
|
|
19
|
+
end
|