sec_id 5.2.0 → 6.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 +4 -4
- data/.yardopts +6 -0
- data/CHANGELOG.md +31 -0
- data/MIGRATION.md +101 -0
- data/README.md +245 -17
- data/lib/sec_id/active_model.rb +144 -0
- data/lib/sec_id/base.rb +64 -2
- data/lib/sec_id/bic/country_codes.rb +46 -0
- data/lib/sec_id/bic.rb +123 -0
- data/lib/sec_id/cei.rb +19 -7
- data/lib/sec_id/cfi/attribute_set.rb +49 -0
- data/lib/sec_id/cfi/classification.rb +107 -0
- data/lib/sec_id/cfi/field.rb +56 -0
- data/lib/sec_id/cfi/tables.rb +1033 -0
- data/lib/sec_id/cfi.rb +138 -208
- data/lib/sec_id/cik.rb +13 -0
- data/lib/sec_id/concerns/checkable.rb +2 -2
- data/lib/sec_id/concerns/generatable.rb +71 -0
- data/lib/sec_id/concerns/normalizable.rb +1 -0
- data/lib/sec_id/concerns/validatable.rb +14 -3
- data/lib/sec_id/cusip.rb +18 -7
- data/lib/sec_id/deep_freeze.rb +18 -0
- data/lib/sec_id/detector.rb +29 -31
- data/lib/sec_id/dti.rb +104 -0
- data/lib/sec_id/figi.rb +18 -0
- data/lib/sec_id/fisn.rb +31 -0
- data/lib/sec_id/iban/country_rules.rb +7 -6
- data/lib/sec_id/iban.rb +19 -1
- data/lib/sec_id/isin.rb +20 -15
- data/lib/sec_id/lei.rb +13 -0
- data/lib/sec_id/occ.rb +21 -0
- data/lib/sec_id/railtie.rb +14 -0
- data/lib/sec_id/scanner.rb +18 -27
- data/lib/sec_id/sedol.rb +16 -0
- data/lib/sec_id/valoren.rb +13 -0
- data/lib/sec_id/version.rb +2 -1
- data/lib/sec_id/wkn.rb +16 -0
- data/lib/sec_id.rb +31 -7
- data/sec_id.gemspec +11 -5
- data/sig/manifest.yaml +8 -0
- data/sig/sec_id/base.rbs +78 -0
- data/sig/sec_id/bic/country_codes.rbs +5 -0
- data/sig/sec_id/bic.rbs +30 -0
- data/sig/sec_id/cei.rbs +26 -0
- data/sig/sec_id/cfi/attribute_set.rbs +68 -0
- data/sig/sec_id/cfi/classification.rbs +23 -0
- data/sig/sec_id/cfi/field.rbs +337 -0
- data/sig/sec_id/cfi/tables.rbs +68 -0
- data/sig/sec_id/cfi.rbs +56 -0
- data/sig/sec_id/cik.rbs +19 -0
- data/sig/sec_id/concerns/checkable.rbs +41 -0
- data/sig/sec_id/concerns/generatable.rbs +26 -0
- data/sig/sec_id/concerns/normalizable.rbs +34 -0
- data/sig/sec_id/concerns/validatable.rbs +42 -0
- data/sig/sec_id/cusip.rbs +29 -0
- data/sig/sec_id/deep_freeze.rbs +6 -0
- data/sig/sec_id/detector.rbs +29 -0
- data/sig/sec_id/dti.rbs +27 -0
- data/sig/sec_id/errors.rbs +28 -0
- data/sig/sec_id/figi.rbs +31 -0
- data/sig/sec_id/fisn.rbs +24 -0
- data/sig/sec_id/iban/country_rules.rbs +10 -0
- data/sig/sec_id/iban.rbs +54 -0
- data/sig/sec_id/isin.rbs +40 -0
- data/sig/sec_id/lei.rbs +33 -0
- data/sig/sec_id/occ.rbs +45 -0
- data/sig/sec_id/scanner.rbs +44 -0
- data/sig/sec_id/sedol.rbs +34 -0
- data/sig/sec_id/valoren.rbs +29 -0
- data/sig/sec_id/version.rbs +3 -0
- data/sig/sec_id/wkn.rbs +15 -0
- data/sig/sec_id.rbs +58 -0
- metadata +55 -7
- data/lib/sec_id/concerns/identifier_metadata.rb +0 -56
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2c705053b66b513f6cbac21702cd4548f5f446669f9251db569d62693698ff64
|
|
4
|
+
data.tar.gz: b792e03a67c57ead4064c392925771033c35b68fd984ad93aa5d932e37d1ea21
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 51880f7c27744b9700e01bca80a6f3a9a436d76a843921fdc97a401e12ef0acb2011c4f67a19034c30ea90e59638de0b66cb8a949b32d2d896e28a4f7957c4ba
|
|
7
|
+
data.tar.gz: 780d1cd07415046d2c5dfda3b6f548785ae0e244bdb294876440521f90475562000beaf493c88109d609e9c65fa83b94ad67c8509084174a84e52e6be9268543
|
data/.yardopts
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,37 @@ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [6.1.0] - 2026-07-10
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `SecID::Base#deconstruct_keys` — every identifier now destructures in `case/in` patterns, exposing the same parsed fields `#to_h` reports under `:components`. The `keys` argument is ignored, no new keys are introduced, and validity is not part of the protocol: `SecID.parse` returning `nil` remains the validity guard, and an instance built from unparseable input binds `nil` for each component. No `deconstruct` (array-pattern) method is defined
|
|
16
|
+
- `SecID::Base.type_key` — the memoized class-level registry symbol for an identifier type, joining the existing `short_name` / `full_name` / `id_length` metadata surface. It round-trips through the registry: `SecID[SecID::ISIN.type_key] == SecID::ISIN`. It is now the single authority for that symbol; the registry, `#to_h`, `SecID.explain`, the ActiveModel validator, the detector, and the scanner all read it instead of each deriving it from the class name
|
|
17
|
+
|
|
18
|
+
## [6.0.0] - 2026-07-08
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Hand-written RBS type signatures under `sig/`, shipped in the gem, so consumers running Steep or an RBS-aware editor resolve sec_id's types on install. The core library is checked by Steep in strict mode with zero errors and verified at runtime against the specs via RBS::Test. New dev/test tooling (`rbs` and `steep`, `require: false` — the gem stays zero-runtime-dependency) and rake tasks: `rake steep` (strict type check), `rake steep:coverage` (untyped-call gate), `rake rbs:test` (runtime signature verification), `rake rbs` (validate signatures, in the default task), and `rake sig:cfi` (regenerate the CFI dynamic-method signatures from `SecID::CFI::Tables`, guarded by a drift spec). CI gains a `types` job on Ruby 4.0. The optional ActiveModel adapter and Railtie are excluded from the typed scope
|
|
23
|
+
- `SecID::CFI#decode` returns a frozen `SecID::CFI::Classification` value object for any valid CFI (`nil` for invalid). Its category, group, and each attribute are `Field` objects carrying the CFI letter (`#code`), semantic symbol (`#name`), and authoritative ISO 10962 label (`#label`), plus `<name>?` predicates scoped to the field's own domain — so `decode.category.equity?` and `decode.attributes.voting_right.voting?` answer, while an out-of-domain predicate (e.g. `category.voting?`) raises `NoMethodError`. `#attributes` is an `Enumerable` of the attribute fields keyed by each position's group meaning (`attributes.voting_right`, nil-safe `attributes[:form]`). Ships a human-readable `#to_s` and `#to_h`/`#as_json` for serialization. `X` decodes to `:not_applicable`; pure-N/A positions are omitted
|
|
24
|
+
- `SecID::CFI` now validates the full ISO 10962:2021 attribute matrix — positions 3–6 are checked per-group, `Strategies` (`K`) codes require `XXXX`, pure-N/A positions accept only `X`, and the `ED` (depositary receipts on equities) cross-position rule is enforced. A new `:invalid_attribute` error code (mapped to `InvalidStructureError`) names the offending positions and group
|
|
25
|
+
- Opt-in ActiveModel / Rails validator (`require 'sec_id/active_model'`), registered as `sec_id`, for declarative validation: `validates :isin, sec_id: { type: :isin }`. Validates a single type (`type:`), an allowlist (`types:`), or any supported type (`sec_id: true`); a bad type raises `ArgumentError` at class-load. Opt-in `normalize: true` accepts separatored input and rewrites the attribute to canonical form on success; opt-in `details: true` surfaces the specific failure reason. Auto-activates in Rails via a Railtie (no `require:`/initializer needed) and adds **no runtime dependency** — nothing on the default `require 'sec_id'` path loads Rails or ActiveModel. Tested across Rails 7.2, 8.0, 8.1, and main
|
|
26
|
+
- BIC / SWIFT code (ISO 9362) support via `SecID::BIC` — validate, normalize, parse, detect, extract, and generate 8- or 11-character Business Identifier Codes. Exposes `bank_code`, `country_code`, `location_code`, and `branch_code` (`nil` for a BIC8) components. Validates the embedded country code against a frozen ISO 3166-1 / SWIFT-recognized set (`SecID::BIC.countries`), raising `InvalidStructureError` (`:invalid_country`) for unrecognized codes. Validation confirms structure and a real country code only — it does not verify registration in the licensed SWIFT registry
|
|
27
|
+
- DTI (ISO 24165, Digital Token Identifier) support via `SecID::DTI` — the gem's 15th identifier type, and the first offline DTI validator in any language. Validates the ISO 7064 hybrid MOD 31,30 check character over the 30-symbol DTI alphabet (digits plus consonants; vowels and `Y` never appear), identified empirically from public registry data (1,595 of 1,596 snapshot codes plus 2 post-snapshot registrations). Bitcoin's hand-assigned code (`4H95J0R2X`, which fails the algorithm) is honored via a frozen exception map consulted by `valid?`, `restore`, and `check_digit` alike. Unlike every other check-digit type in the gem, `check_digit`/`calculate_check_digit` return a `String` rather than an `Integer`, since DTI check characters can be letters
|
|
28
|
+
- `.generate` on all 15 identifier types plus a central `SecID.generate(:type)` dispatcher for producing syntactically valid identifiers (with correct check digits where applicable) as test fixtures — accepts an optional `random:` keyword (a Ruby `Random`) for reproducible output. Generated values are valid in format only and are not real, registered securities
|
|
29
|
+
- 100% YARD documentation coverage of the public API, enforced in CI via `rake yard:stats`. The gem now ships a `.yardopts` so `rubydoc.info` renders the public API cleanly (private internals hidden, README/CHANGELOG/LICENSE as pages) and exposes `documentation_uri` metadata (a Documentation link on the RubyGems page). Adds a `yard` dev/test dependency (`require: false` — the gem stays zero-runtime-dependency) and a `rake yard` task for local HTML docs
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- Internal `@api private` IBAN country-data module dissolved into the `SecID::IBAN` class, so constant names match their file paths per Ruby convention: `SecID::IBANCountryRules::COUNTRY_RULES` / `LENGTH_ONLY_COUNTRIES` are now `SecID::IBAN::COUNTRY_RULES` / `SecID::IBAN::LENGTH_ONLY_COUNTRIES`
|
|
34
|
+
- `SecID.valid?` and `SecID.parse` are faster and allocate less — `SecID.valid?` short-circuits on the first matching type instead of running a full detect-and-sort, and `SecID.parse` reuses the instance built during detection instead of instantiating the matched type a second time
|
|
35
|
+
- **BREAKING:** `SecID::CFI.valid?` is now strict at the attribute level for all 14 categories (including the derivative categories `S`, `H`, `J`) — codes with attribute letters outside the ISO 10962:2021 tables, previously accepted, are now invalid (e.g. `CFI.valid?('ESZZZZ')` returns `false`). There is no leniency option, matching every other identifier type in the gem. The ActiveModel validator inherits this strictness automatically
|
|
36
|
+
- **BREAKING:** `SecID::CFI` group tables corrected to ISO 10962:2021 — six categories carried wrong group letters. Non-listed options (`H`) are now classified by underlying (`HR` Rates, `HT` Commodities, `HE` Equity, `HC` Credit, `HF` FX, `HM` Others) instead of the copied listed-option call/put shape; `D` gains `DE` (structured products without capital protection) and corrects `DG`/`DA`/`DN` (dropping the invented `municipal_notes`); `L` becomes `LL`/`LR`/`LS`, `T` becomes `TC`/`TT`/`TR`/`TI`/…, and the phantom `FM`/`IM`/`JM`/`LM` groups are removed. Group symbols renamed accordingly (e.g. `LS` → `:securities_lending`, `TI` → `:indices`, `MM` → `:other_assets`). Real `H`-category codes that were rejected now validate; codes relying on the old wrong letters no longer do
|
|
37
|
+
|
|
38
|
+
### Removed
|
|
39
|
+
|
|
40
|
+
- **BREAKING:** the 12 hardcoded equity predicate helpers on `SecID::CFI` (`equity?`, `voting?`, `non_voting?`, `restricted_voting?`, `enhanced_voting?`, `restrictions?`, `no_restrictions?`, `fully_paid?`, `nil_paid?`, `partly_paid?`, `bearer?`, `registered?`). They were category-wide and semantically wrong for non-equity groups. Migration: use `cfi.decode` and its scoped field predicates — `cfi.voting?` → `cfi.decode.attributes.voting_right.voting?` — which answer only on the field whose ISO domain defines the concept. Two names do not carry over name-for-name: migrate `cfi.equity?` to `cfi.decode.category.equity?` (or `cfi.category == :equity`, unchanged), and `cfi.no_restrictions?` to `cfi.decode.attributes.ownership_restrictions.free_of_restrictions?` (the ISO value name)
|
|
41
|
+
|
|
11
42
|
## [5.2.0] - 2026-02-24
|
|
12
43
|
|
|
13
44
|
### Added
|
data/MIGRATION.md
CHANGED
|
@@ -1,3 +1,104 @@
|
|
|
1
|
+
# Upgrading to SecID 6.0
|
|
2
|
+
|
|
3
|
+
This guide covers all breaking changes when upgrading from SecID 5.x to 6.0. Every one is
|
|
4
|
+
confined to `SecID::CFI`, which became a full ISO 10962:2021 classifier (strict attribute
|
|
5
|
+
validation, corrected group tables, and a new `#decode` classification object replacing the
|
|
6
|
+
old category-wide predicates).
|
|
7
|
+
|
|
8
|
+
## Quick Reference
|
|
9
|
+
|
|
10
|
+
| What changed | Before | After |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| CFI attribute validation is strict | `CFI.valid?('ESZZZZ')` was `true` | now `false` — positions 3-6 must match the ISO tables for the group |
|
|
13
|
+
| CFI group letters/symbols corrected | old `H`/`D`/`L`/`T` groups; `FM`/`IM`/`JM`/`LM` | ISO 10962:2021 groups; e.g. `LS` → `:securities_lending`, `TI` → `:indices` |
|
|
14
|
+
| Equity predicates removed | `cfi.voting?`, `cfi.fully_paid?`, … | `cfi.decode.attributes.voting_right.voting?`, … |
|
|
15
|
+
| `equity?` (category-wide) removed | `cfi.equity?` | `cfi.decode.category.equity?` |
|
|
16
|
+
| `no_restrictions?` uses ISO name | `cfi.no_restrictions?` | `cfi.decode.attributes.ownership_restrictions.free_of_restrictions?` |
|
|
17
|
+
|
|
18
|
+
## Step-by-Step
|
|
19
|
+
|
|
20
|
+
### 1. Update Gemfile
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
gem 'sec_id', '~> 6.0'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Then run `bundle update sec_id`.
|
|
27
|
+
|
|
28
|
+
### 2. Expect stricter CFI validation
|
|
29
|
+
|
|
30
|
+
`SecID::CFI.valid?` now validates positions 3-6 against the ISO 10962:2021 attribute tables
|
|
31
|
+
for every category, not just the code's shape. Codes carrying attribute letters outside the
|
|
32
|
+
tables were accepted before and are now invalid. There is no leniency option, matching every
|
|
33
|
+
other identifier type.
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
# Before (5.x) — shape-only
|
|
37
|
+
SecID::CFI.valid?('ESZZZZ') # => true
|
|
38
|
+
|
|
39
|
+
# After (6.0) — Z is not a permissible equity attribute
|
|
40
|
+
SecID::CFI.valid?('ESZZZZ') # => false
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Conversely, real codes the old (incorrect) group tables rejected — notably non-listed options
|
|
44
|
+
(`H`), now classified by underlying (`HR` Rates, `HT` Commodities, `HE` Equity, `HC` Credit,
|
|
45
|
+
`HF` FX, `HM` Miscellaneous) — now validate. If you generate CFIs as test fixtures,
|
|
46
|
+
`SecID::CFI.generate` now samples only table-permitted letters, so every generated code is valid.
|
|
47
|
+
|
|
48
|
+
### 3. Replace removed CFI predicates with `cfi.decode`
|
|
49
|
+
|
|
50
|
+
The 12 hardcoded equity predicates (`equity?`, `voting?`, `non_voting?`, `restricted_voting?`,
|
|
51
|
+
`enhanced_voting?`, `restrictions?`, `no_restrictions?`, `fully_paid?`, `nil_paid?`,
|
|
52
|
+
`partly_paid?`, `bearer?`, `registered?`) are removed — they were category-wide and
|
|
53
|
+
semantically wrong for non-equity groups. `cfi.decode` returns a classification whose
|
|
54
|
+
category, group, and attributes are each a **field** object; a predicate lives on the field
|
|
55
|
+
whose ISO domain defines it, so it can only answer where the concept is meaningful.
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
# Before (5.x)
|
|
59
|
+
cfi = SecID::CFI.new('ESVUFR')
|
|
60
|
+
cfi.voting? # => true
|
|
61
|
+
cfi.fully_paid? # => true
|
|
62
|
+
|
|
63
|
+
# After (6.0)
|
|
64
|
+
d = SecID::CFI.new('ESVUFR').decode
|
|
65
|
+
d.attributes.voting_right.voting? # => true
|
|
66
|
+
d.attributes.payment_status.fully_paid? # => true
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Each field exposes `#code` (the CFI letter), `#name` (the symbol), `#label` (the ISO string),
|
|
70
|
+
and `<name>?` predicates scoped to its own domain — asking a field a predicate outside its
|
|
71
|
+
domain raises `NoMethodError` (e.g. `d.attributes.payment_status.voting?`), which is what
|
|
72
|
+
makes the answers unambiguous. `attributes` is an `Enumerable`; look positions up by meaning
|
|
73
|
+
with `d.attributes.voting_right` or the nil-safe `d.attributes[:voting_right]`.
|
|
74
|
+
|
|
75
|
+
`decode` returns `nil` for an invalid CFI, so guard before chaining:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
SecID::CFI.new('QQXXXX').decode # => nil
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Two names do not map name-for-name:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
# `equity?` was category-wide, not an attribute value:
|
|
85
|
+
cfi.equity? # before
|
|
86
|
+
cfi.decode.category.equity? # after (or cfi.category == :equity — CFI#category is unchanged)
|
|
87
|
+
|
|
88
|
+
# `no_restrictions?` uses the ISO value name, on the ownership_restrictions field:
|
|
89
|
+
cfi.no_restrictions? # before
|
|
90
|
+
cfi.decode.attributes.ownership_restrictions.free_of_restrictions? # after
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 4. Check group symbols if you branch on them
|
|
94
|
+
|
|
95
|
+
Several group symbols changed to match ISO 10962:2021 (e.g. `LS` → `:securities_lending`,
|
|
96
|
+
`TI` → `:indices`, `MM` → `:other_assets`) and the phantom `FM`/`IM`/`JM`/`LM` groups were
|
|
97
|
+
removed. If you match on `cfi.group`, review `SecID::CFI.groups_for(category_code)` for the
|
|
98
|
+
current values.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
1
102
|
# Upgrading to SecID 5.0
|
|
2
103
|
|
|
3
104
|
This guide covers all breaking changes when upgrading from SecID 4.x to 5.0.
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# SecID [](https://rubygems.org/gems/sec_id) [](https://github.com/svyatov/sec_id/actions/workflows/main.yml) [](https://codecov.io/gh/svyatov/sec_id) [](https://rubydoc.info/gems/sec_id) [](https://www.ruby-lang.org) [](https://github.com/svyatov/sec_id/tree/main/sig)
|
|
2
2
|
|
|
3
|
-
> A Ruby toolkit for securities identifiers — validate, parse, normalize, detect, and
|
|
3
|
+
> A Ruby toolkit for securities identifiers — validate, parse, normalize, detect, convert, generate, and classify.
|
|
4
4
|
|
|
5
5
|
## Table of Contents
|
|
6
6
|
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
- [Text Scanning](#text-scanning) - find identifiers in freeform text
|
|
12
12
|
- [Debugging Detection](#debugging-detection) - understand why strings match or don't
|
|
13
13
|
- [Structured Validation](#structured-validation) - detailed error codes and messages
|
|
14
|
+
- [Pattern Matching](#pattern-matching) - destructure identifiers with `case/in`
|
|
15
|
+
- [Generating Test Fixtures](#generating-test-fixtures) - produce valid identifiers for tests
|
|
14
16
|
- [ISIN](#isin) - International Securities Identification Number
|
|
15
17
|
- [CUSIP](#cusip) - Committee on Uniform Securities Identification Procedures
|
|
16
18
|
- [CEI](#cei) - CUSIP Entity Identifier
|
|
@@ -24,7 +26,11 @@
|
|
|
24
26
|
- [Valoren](#valoren) - Swiss Security Number
|
|
25
27
|
- [CFI](#cfi) - Classification of Financial Instruments
|
|
26
28
|
- [FISN](#fisn) - Financial Instrument Short Name
|
|
29
|
+
- [BIC](#bic) - Business Identifier Code / SWIFT code
|
|
30
|
+
- [DTI](#dti) - Digital Token Identifier
|
|
31
|
+
- [ActiveModel / Rails Validator](#activemodel--rails-validator) - declarative `validates :isin, sec_id: {...}`
|
|
27
32
|
- [Lookup Service Integration](#lookup-service-integration)
|
|
33
|
+
- [Type Signatures (RBS)](#type-signatures-rbs)
|
|
28
34
|
- [Development](#development)
|
|
29
35
|
- [Contributing](#contributing)
|
|
30
36
|
- [Changelog](#changelog)
|
|
@@ -40,7 +46,7 @@ Ruby 3.2+ is required.
|
|
|
40
46
|
Add this line to your application's Gemfile:
|
|
41
47
|
|
|
42
48
|
```ruby
|
|
43
|
-
gem 'sec_id', '~>
|
|
49
|
+
gem 'sec_id', '~> 6.0'
|
|
44
50
|
```
|
|
45
51
|
|
|
46
52
|
And then execute:
|
|
@@ -70,6 +76,7 @@ All identifier classes provide `valid?`, `errors`, `validate`, `validate!` metho
|
|
|
70
76
|
**All identifiers** support hash serialization:
|
|
71
77
|
- `#to_h` - returns a hash with `:type`, `:full_id`, `:normalized`, `:valid`, and `:components` keys
|
|
72
78
|
- `#as_json` - same as `#to_h`, for JSON serialization compatibility (Rails, `JSON.generate`, etc.)
|
|
79
|
+
- `#deconstruct_keys` - exposes `:components` for [pattern matching](#pattern-matching) with `case/in`
|
|
73
80
|
|
|
74
81
|
```ruby
|
|
75
82
|
SecID::ISIN.new('US5949181045').to_h
|
|
@@ -95,7 +102,7 @@ a.eql?(b) # => true
|
|
|
95
102
|
Set.new([a, b]).size # => 1
|
|
96
103
|
```
|
|
97
104
|
|
|
98
|
-
**Check-digit based identifiers** (ISIN, CUSIP, CEI, SEDOL, FIGI, LEI, IBAN) also provide:
|
|
105
|
+
**Check-digit based identifiers** (ISIN, CUSIP, CEI, SEDOL, FIGI, LEI, IBAN, DTI) also provide:
|
|
99
106
|
- `restore` / `.restore` - returns the full identifier string with correct check-digit (no mutation)
|
|
100
107
|
- `restore!` / `.restore!` - restores check-digit in place and returns `self` / instance
|
|
101
108
|
- `check_digit` / `calculate_check_digit` - calculates and returns the check-digit
|
|
@@ -114,6 +121,7 @@ SecID.identifiers # => [SecID::ISIN, SecID::CUSIP, ...]
|
|
|
114
121
|
SecID.identifiers.map(&:short_name) # => ["ISIN", "CUSIP", "SEDOL", ...]
|
|
115
122
|
|
|
116
123
|
# Query metadata
|
|
124
|
+
SecID::ISIN.type_key # => :isin (SecID[SecID::ISIN.type_key] == SecID::ISIN)
|
|
117
125
|
SecID::ISIN.short_name # => "ISIN"
|
|
118
126
|
SecID::ISIN.full_name # => "International Securities Identification Number"
|
|
119
127
|
SecID::ISIN.id_length # => 12
|
|
@@ -122,7 +130,7 @@ SecID::ISIN.has_check_digit? # => true
|
|
|
122
130
|
|
|
123
131
|
# Filter with standard Ruby
|
|
124
132
|
SecID.identifiers.select(&:has_check_digit?).map(&:short_name)
|
|
125
|
-
# => ["ISIN", "CUSIP", "SEDOL", "FIGI", "LEI", "IBAN", "CEI"]
|
|
133
|
+
# => ["ISIN", "CUSIP", "SEDOL", "FIGI", "LEI", "IBAN", "CEI", "DTI"]
|
|
126
134
|
|
|
127
135
|
# Detect identifier type from an unknown string
|
|
128
136
|
# Results are sorted by specificity: check-digit types first, then by length precision
|
|
@@ -179,8 +187,9 @@ match.raw # => "US-5949-1810-45"
|
|
|
179
187
|
match.identifier.normalized # => "US5949181045"
|
|
180
188
|
```
|
|
181
189
|
|
|
182
|
-
> **Known limitations:** Format-only types (CIK, Valoren, WKN,
|
|
183
|
-
> common numbers and short words in prose
|
|
190
|
+
> **Known limitations:** Format-only types (CIK, Valoren, WKN, BIC) can false-positive on
|
|
191
|
+
> common numbers and short words in prose (a BIC8 is 8 letters with a valid country code in the
|
|
192
|
+
> middle) — use the `types:` filter to restrict scanning when
|
|
184
193
|
> this is a concern. Identifiers prefixed with special characters (e.g. `#US5949181045`) may be
|
|
185
194
|
> consumed as a single token by CUSIP's `*@#` character class and fail validation, preventing
|
|
186
195
|
> the embedded identifier from being found.
|
|
@@ -224,12 +233,14 @@ SecID::ISIN.validate('US5949181040').errors # => #<SecID::Errors>
|
|
|
224
233
|
- `:invalid_format` - correct length and characters but wrong structure
|
|
225
234
|
|
|
226
235
|
**Type-specific error codes:**
|
|
227
|
-
- `:invalid_check_digit` - check digit mismatch (ISIN, CUSIP, SEDOL, FIGI, LEI, IBAN, CEI)
|
|
236
|
+
- `:invalid_check_digit` - check digit mismatch (ISIN, CUSIP, SEDOL, FIGI, LEI, IBAN, CEI, DTI)
|
|
228
237
|
- `:invalid_prefix` - restricted FIGI prefix (FIGI)
|
|
229
238
|
- `:invalid_category` - unknown CFI category code (CFI)
|
|
230
239
|
- `:invalid_group` - unknown CFI group code for category (CFI)
|
|
240
|
+
- `:invalid_attribute` - impermissible attribute letter for the group, or `K` code without `XXXX` (CFI)
|
|
231
241
|
- `:invalid_bban` - BBAN format invalid for country (IBAN)
|
|
232
242
|
- `:invalid_date` - unparseable expiration date (OCC)
|
|
243
|
+
- `:invalid_country` - unrecognized ISO 3166 / SWIFT country code (BIC)
|
|
233
244
|
|
|
234
245
|
#### Fail-fast validation with `validate!`
|
|
235
246
|
|
|
@@ -253,6 +264,69 @@ SecID::FIGI.new('BSG000BLNNH6').validate!
|
|
|
253
264
|
isin = SecID::ISIN.validate!('US5949181045') # => #<SecID::ISIN>
|
|
254
265
|
```
|
|
255
266
|
|
|
267
|
+
### Pattern Matching
|
|
268
|
+
|
|
269
|
+
Every identifier destructures in `case/in` via its parsed components. Since `SecID.parse` returns `nil` for
|
|
270
|
+
anything invalid, an `in nil` branch is a complete validity guard:
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
case SecID.parse(input)
|
|
274
|
+
in SecID::ISIN[country_code: 'US', nsin:] then "US security #{nsin}"
|
|
275
|
+
in SecID::ISIN[country_code:] then "foreign security from #{country_code}"
|
|
276
|
+
in SecID::OCC[underlying:, type:, strike_mills:] then "#{type} option on #{underlying}"
|
|
277
|
+
in nil then 'not a valid identifier'
|
|
278
|
+
end
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
`SecID::Match` is a `Data`, so scan results destructure one layer up and nest into the identifier they wrap:
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
SecID.extract('AAPL ISIN US0378331005').each do |match|
|
|
285
|
+
case match
|
|
286
|
+
in { type: :isin, identifier: SecID::ISIN[country_code: 'US'] } then puts 'US ISIN found'
|
|
287
|
+
else next
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
The keys are exactly the `:components` of `#to_h`, and destructuring never consults
|
|
293
|
+
`valid?`. An instance built directly from unparseable input therefore binds `nil` for every component, the same shape
|
|
294
|
+
`to_h` reports:
|
|
295
|
+
|
|
296
|
+
```ruby
|
|
297
|
+
SecID::ISIN.new('GARBAGE') => { country_code:, nsin: }
|
|
298
|
+
country_code # => nil
|
|
299
|
+
nsin # => nil
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
CIK, WKN, and Valoren have no sub-fields: they match a bare `in SecID::CIK` but no keyed pattern. Array patterns
|
|
303
|
+
(`deconstruct`) are not supported.
|
|
304
|
+
|
|
305
|
+
Watch the `:type` key: on a `SecID::Match` it is the registry symbol (`:occ`), while on an `OCC` instance it is the
|
|
306
|
+
OSI option type (`'C'` or `'P'`), so `in SecID::OCC[type: :occ]` never matches. Other types have no `:type` component
|
|
307
|
+
at all — `#to_h`'s envelope keys (`:type`, `:full_id`, `:normalized`, `:valid`) are not part of the pattern surface.
|
|
308
|
+
|
|
309
|
+
### Generating Test Fixtures
|
|
310
|
+
|
|
311
|
+
Generate syntactically valid identifiers — with correct check digits where applicable — for use as test fixtures. Available per class and via the central dispatcher:
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
SecID::ISIN.generate # => #<SecID::ISIN ...>
|
|
315
|
+
SecID::ISIN.generate.valid? # => true
|
|
316
|
+
|
|
317
|
+
# Central dispatcher by type symbol (mirrors SecID[])
|
|
318
|
+
SecID.generate(:cusip) # => #<SecID::CUSIP ...>
|
|
319
|
+
SecID.generate(:nope) # => raises ArgumentError: Unknown identifier type: :nope
|
|
320
|
+
|
|
321
|
+
# Pass a seeded Random for reproducible output
|
|
322
|
+
SecID::LEI.generate(random: Random.new(42)) == SecID::LEI.generate(random: Random.new(42)) # => true
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
> **Generated identifiers are valid in format only — they are not real, registered securities.**
|
|
326
|
+
> Country codes, FIGI prefixes, OCC expiry dates, and CFI category/group/attribute choices are
|
|
327
|
+
> randomly selected (from the values each standard permits) and do not map to real-world
|
|
328
|
+
> instruments. Use them as test fixtures, not as references to actual securities.
|
|
329
|
+
|
|
256
330
|
### ISIN
|
|
257
331
|
|
|
258
332
|
> [International Securities Identification Number](https://en.wikipedia.org/wiki/International_Securities_Identification_Number) - a 12-character alphanumeric code that uniquely identifies a security.
|
|
@@ -554,12 +628,12 @@ valoren.to_isin('LI') # => #<SecID::ISIN> (LI ISIN)
|
|
|
554
628
|
|
|
555
629
|
### CFI
|
|
556
630
|
|
|
557
|
-
> [Classification of Financial Instruments](https://en.wikipedia.org/wiki/ISO_10962) - a 6-character alphabetic code that classifies financial instruments per ISO 10962.
|
|
631
|
+
> [Classification of Financial Instruments](https://en.wikipedia.org/wiki/ISO_10962) - a 6-character alphabetic code that classifies financial instruments per ISO 10962:2021.
|
|
558
632
|
|
|
559
633
|
```ruby
|
|
560
634
|
# class level
|
|
561
|
-
SecID::CFI.valid?('ESXXXX') # => true
|
|
562
635
|
SecID::CFI.valid?('ESVUFR') # => true
|
|
636
|
+
SecID::CFI.valid?('ESZZZZ') # => false (Z is not a permissible equity attribute)
|
|
563
637
|
# instance level
|
|
564
638
|
cfi = SecID::CFI.new('ESVUFR')
|
|
565
639
|
cfi.full_id # => 'ESVUFR'
|
|
@@ -570,15 +644,28 @@ cfi.category # => :equity
|
|
|
570
644
|
cfi.group # => :common_shares
|
|
571
645
|
cfi.valid? # => true
|
|
572
646
|
|
|
573
|
-
#
|
|
574
|
-
cfi.
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
647
|
+
# Decode the full ISO 10962:2021 classification
|
|
648
|
+
d = cfi.decode
|
|
649
|
+
d.category.name # => :equity
|
|
650
|
+
d.category.label # => 'Equities'
|
|
651
|
+
d.category.equity? # => true (scoped to the category domain)
|
|
652
|
+
d.group.name # => :common_shares
|
|
653
|
+
d.group.label # => 'Common/Ordinary shares'
|
|
654
|
+
|
|
655
|
+
# attributes is an Enumerable of fields keyed by each position's group meaning
|
|
656
|
+
d.attributes.map(&:name) # => [:voting, :free_of_restrictions, :fully_paid, :registered]
|
|
657
|
+
d.attributes.voting_right.name # => :voting
|
|
658
|
+
d.attributes.voting_right.voting? # => true (scoped to that position's values)
|
|
659
|
+
d.attributes.payment_status.label # => 'Fully paid'
|
|
660
|
+
d.attributes[:form].code # => 'R' (nil-safe lookup; .form also works)
|
|
661
|
+
d.to_s # => 'Equities / Common/Ordinary shares: Voting, Free of restrictions, Fully paid, Registered'
|
|
662
|
+
|
|
663
|
+
SecID::CFI.new('QQXXXX').decode # => nil (decode returns nil for an invalid CFI)
|
|
579
664
|
```
|
|
580
665
|
|
|
581
|
-
CFI
|
|
666
|
+
CFI is validated strictly against the ISO 10962:2021 code tables for all 14 categories: the category (position 1), the group (position 2), and every attribute (positions 3-6) must be a value the standard defines for that group. `X` means "not applicable" and is accepted in every position; `Strategies` (`K`) codes carry no attributes and require `XXXX`. An impermissible attribute letter raises `InvalidStructureError` (`:invalid_attribute`).
|
|
667
|
+
|
|
668
|
+
> **Migration from < 6.0:** the old category-wide equity predicates (`cfi.voting?`, `cfi.fully_paid?`, …) are removed. Use `cfi.decode` and its scoped fields instead — a predicate now lives on the field whose domain defines it: `cfi.voting?` → `cfi.decode.attributes.voting_right.voting?`. Two do not map name-for-name: `cfi.equity?` → `cfi.decode.category.equity?` (or `cfi.category == :equity`), and `cfi.no_restrictions?` → `cfi.decode.attributes.ownership_restrictions.free_of_restrictions?`. Several group letters and symbols also changed to match ISO 10962:2021 (e.g. non-listed options `H` are now classified by underlying, and `LS` → `:securities_lending`, `TI` → `:indices`).
|
|
582
669
|
|
|
583
670
|
```ruby
|
|
584
671
|
# Introspect valid codes
|
|
@@ -606,6 +693,121 @@ fisn.to_s # => 'APPLE INC/SH'
|
|
|
606
693
|
|
|
607
694
|
FISN format: `Issuer Name/Abbreviated Instrument Description` with issuer (1-15 chars) and description (1-19 chars) separated by a forward slash. Character set: uppercase A-Z, digits 0-9, and space.
|
|
608
695
|
|
|
696
|
+
### BIC
|
|
697
|
+
|
|
698
|
+
> [Business Identifier Code](https://en.wikipedia.org/wiki/ISO_9362) - an 8- or 11-character code identifying financial and non-financial institutions per ISO 9362 (also known as a SWIFT code).
|
|
699
|
+
|
|
700
|
+
```ruby
|
|
701
|
+
# class level
|
|
702
|
+
SecID::BIC.valid?('DEUTDEFF') # => true (BIC8)
|
|
703
|
+
SecID::BIC.valid?('DEUTDEFF500') # => true (BIC11)
|
|
704
|
+
|
|
705
|
+
# instance level
|
|
706
|
+
bic = SecID::BIC.new('DEUTDEFF500')
|
|
707
|
+
bic.full_id # => 'DEUTDEFF500'
|
|
708
|
+
bic.identifier # => 'DEUTDEFF500'
|
|
709
|
+
bic.bank_code # => 'DEUT'
|
|
710
|
+
bic.country_code # => 'DE'
|
|
711
|
+
bic.location_code # => 'FF'
|
|
712
|
+
bic.branch_code # => '500' (nil for a BIC8)
|
|
713
|
+
bic.valid? # => true
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
BIC accepts exactly 8 or 11 characters: a 4-letter institution code, 2-letter country code, 2-alphanumeric location code, and (for BIC11) a 3-alphanumeric branch code. The country code (positions 5-6) is validated against a frozen ISO 3166-1 / SWIFT-recognized set; a well-formed BIC with an unrecognized country is an `:invalid_country` structural error.
|
|
717
|
+
|
|
718
|
+
```ruby
|
|
719
|
+
SecID::BIC.valid?('DEUTZZFF') # => false ('ZZ' is not a recognized country)
|
|
720
|
+
SecID::BIC.countries # => ['AD', 'AE', 'AF', ...] (sorted, includes 'XK')
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
BIC validation confirms structure and a real country code only. It does **not** verify that the institution, location, or branch corresponds to a registered SWIFT participant — that requires the licensed SWIFT registry.
|
|
724
|
+
|
|
725
|
+
### DTI
|
|
726
|
+
|
|
727
|
+
> [Digital Token Identifier](https://www.dtif.org) - a 9-character code identifying digital tokens (e.g. cryptocurrencies) per ISO 24165, used in EU MiCA/ESMA regulatory reporting.
|
|
728
|
+
|
|
729
|
+
```ruby
|
|
730
|
+
# class level
|
|
731
|
+
SecID::DTI.valid?('X9J9K872S') # => true
|
|
732
|
+
SecID::DTI.restore('X9J9K872') # => 'X9J9K872S'
|
|
733
|
+
SecID::DTI.restore!('X9J9K872') # => #<SecID::DTI>
|
|
734
|
+
SecID::DTI.check_digit('X9J9K872') # => 'S'
|
|
735
|
+
|
|
736
|
+
# instance level
|
|
737
|
+
dti = SecID::DTI.new('X9J9K872S')
|
|
738
|
+
dti.full_id # => 'X9J9K872S'
|
|
739
|
+
dti.identifier # => 'X9J9K872'
|
|
740
|
+
dti.check_digit # => 'S'
|
|
741
|
+
dti.valid? # => true
|
|
742
|
+
dti.restore # => 'X9J9K872S'
|
|
743
|
+
dti.restore! # => #<SecID::DTI> (mutates instance)
|
|
744
|
+
dti.calculate_check_digit # => 'S'
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
DTI accepts exactly 9 characters: an 8-character base (first character never `0`) plus 1 check character, both drawn from a 30-symbol alphabet — digits `0`-`9` and consonants (vowels and `Y` never appear). Unlike every other check-digit type in this gem, `check_digit` and `calculate_check_digit` return a `String`, not an `Integer`. The check character is computed fully offline via ISO 7064 hybrid MOD 31,30 — no registry lookup or paywalled ISO 24165-1 spec required.
|
|
748
|
+
|
|
749
|
+
> **Grandfathered code:** Bitcoin's registered code (`4H95J0R2X`) predates the algorithm and fails the MOD 31,30 computation (which yields `4H95J0R2T`). A frozen exception map honors the registry's assignment across `valid?`, `restore`, and `check_digit` alike:
|
|
750
|
+
>
|
|
751
|
+
> ```ruby
|
|
752
|
+
> SecID::DTI.valid?('4H95J0R2X') # => true (registered code, via the exception map)
|
|
753
|
+
> SecID::DTI.valid?('4H95J0R2T') # => false (algorithmic form is not the registered one)
|
|
754
|
+
> ```
|
|
755
|
+
|
|
756
|
+
## ActiveModel / Rails Validator
|
|
757
|
+
|
|
758
|
+
SecID ships an opt-in [ActiveModel](https://api.rubyonrails.org/classes/ActiveModel/Validations.html) validator, registered as `sec_id`, for declarative validation of any supported identifier type. It adds **no runtime dependency** — `require 'sec_id'` loads none of it, and ActiveModel is a development/test dependency only.
|
|
759
|
+
|
|
760
|
+
**In Rails it just works.** A Railtie loads the validator automatically after the framework boots, so `gem 'sec_id'` in your `Gemfile` is enough — no `require:` option and no initializer:
|
|
761
|
+
|
|
762
|
+
```ruby
|
|
763
|
+
class Security < ApplicationRecord
|
|
764
|
+
validates :isin, sec_id: { type: :isin }
|
|
765
|
+
end
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
Outside Rails (e.g. Hanami, Sinatra + ActiveModel), require the adapter explicitly:
|
|
769
|
+
|
|
770
|
+
```ruby
|
|
771
|
+
require 'sec_id/active_model'
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
### Validation modes
|
|
775
|
+
|
|
776
|
+
```ruby
|
|
777
|
+
# Single type — the value must be a valid ISIN
|
|
778
|
+
validates :isin, sec_id: { type: :isin }
|
|
779
|
+
|
|
780
|
+
# Allowlist — valid as at least one of the listed types
|
|
781
|
+
validates :ref, sec_id: { types: %i[isin cusip] }
|
|
782
|
+
|
|
783
|
+
# Type-agnostic — valid as any supported type
|
|
784
|
+
validates :ref, sec_id: true
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
An unknown `type:`/`types:` symbol raises `ArgumentError` when the model class loads (fail-fast on misconfiguration), not at validation time.
|
|
788
|
+
|
|
789
|
+
### Strict by default; `normalize:` for lenient input
|
|
790
|
+
|
|
791
|
+
Validation is strict by default, so separatored input like `"US-0378331005"` is invalid. Pass `normalize: true` to accept spaces/hyphens **and** rewrite the attribute to its canonical form on success (a failing value is left untouched):
|
|
792
|
+
|
|
793
|
+
```ruby
|
|
794
|
+
validates :isin, sec_id: { type: :isin, normalize: true }
|
|
795
|
+
# "us-0378331005" validates, and the attribute afterward reads "US0378331005"
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
With `normalize: true` in allowlist or agnostic mode, a value valid as more than one type is written in the canonical form of the **first** matching type (allowlist order, or registration order when agnostic). Prefer a single `type:` when the input is ambiguous across types that normalize differently (e.g. a bare numeric string valid as both CIK and Valoren).
|
|
799
|
+
|
|
800
|
+
### Error messages and `details:`
|
|
801
|
+
|
|
802
|
+
On failure the validator adds one error under the `:sec_id` key with a type-aware default ("is not a valid ISIN" for a single type, "is not a valid securities identifier" for an allowlist/agnostic). Override the message in either of two ways: pass the standard `message:` option (the simplest, per-validation override), or define the attribute-scoped i18n key `activemodel.errors.models.<model>.attributes.<attribute>.sec_id` in your locale files. (The generic `activemodel.errors.messages.sec_id` key is not consulted, because the built-in default is supplied as ActiveModel's `message:` fallback.) Pass `details: true` (with a single `type:` — it is ignored for an allowlist/agnostic) to surface sec_id's specific reason instead of the generic text:
|
|
803
|
+
|
|
804
|
+
```ruby
|
|
805
|
+
validates :isin, sec_id: { type: :isin, details: true }
|
|
806
|
+
# a bad check digit reports e.g. "Check digit '4' is invalid, expected '5'"
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
Standard `EachValidator` options — `allow_nil`, `allow_blank`, `if`, `unless`, `on` — work as usual. Tested against Rails 7.2, 8.0, and 8.1.
|
|
810
|
+
|
|
609
811
|
## Lookup Service Integration
|
|
610
812
|
|
|
611
813
|
SecID validates identifiers but does not include HTTP clients. The [`docs/guides/`](docs/guides/) directory provides integration patterns for external lookup services using only stdlib (`net/http`, `json`):
|
|
@@ -619,6 +821,32 @@ SecID validates identifiers but does not include HTTP clients. The [`docs/guides
|
|
|
619
821
|
|
|
620
822
|
Each guide includes a complete adapter class and a [runnable example](examples/).
|
|
621
823
|
|
|
824
|
+
## Type Signatures (RBS)
|
|
825
|
+
|
|
826
|
+
sec_id ships hand-written [RBS](https://github.com/ruby/rbs) signatures under `sig/`,
|
|
827
|
+
packaged in the gem — so if you use [Steep](https://github.com/soutaro/steep) or an
|
|
828
|
+
RBS-aware editor, sec_id's types resolve automatically on install, no `rbs collection`
|
|
829
|
+
entry required. The only standard-library signature referenced is `date`, declared in
|
|
830
|
+
`sig/manifest.yaml`.
|
|
831
|
+
|
|
832
|
+
The core library is checked by Steep in strict mode and verified at runtime against the
|
|
833
|
+
test suite via RBS::Test. The optional ActiveModel/Rails adapter is excluded from the
|
|
834
|
+
typed scope (it lives off the default require path).
|
|
835
|
+
|
|
836
|
+
Contributor commands:
|
|
837
|
+
|
|
838
|
+
```bash
|
|
839
|
+
bundle exec rake rbs # validate the signatures (also part of `rake`)
|
|
840
|
+
bundle exec rake steep # strict Steep type check
|
|
841
|
+
bundle exec rake steep:coverage # untyped-call coverage gate
|
|
842
|
+
bundle exec rake rbs:test # verify runtime values against the signatures
|
|
843
|
+
bundle exec rake sig:cfi # regenerate the CFI dynamic-method signatures
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
The CFI per-instance dynamic methods (`Field` predicates and `AttributeSet` readers) are
|
|
847
|
+
generated from `SecID::CFI::Tables` by `rake sig:cfi`; a drift-guard spec fails if the
|
|
848
|
+
committed signatures fall out of sync with the tables.
|
|
849
|
+
|
|
622
850
|
## Development
|
|
623
851
|
|
|
624
852
|
After checking out the repo, run `bin/setup` to install dependencies.
|