codes_iso_17442/lib.rs
1/*!
2This package contains an implementation of the
3[ISO 17442](https://www.iso.org/standard/78829.html)
4Legal Entity Identifier (LEI) specification.
5
6The Legal Entity Identifier (LEI) is a unique global identifier for legal
7entities participating in financial transactions. Also known as an LEI code
8or LEI number, its purpose is to help identify legal entities on a globally
9accessible database. Legal entities are organisations such as companies or
10government entities that participate in financial transactions. The identifier
11is used in regulatory reporting to financial regulators and all financial
12companies and funds are required to have an LEI.
13
14
15The ISO Specification defines a format for describing identifiers and
16including check digits to ensure validity. In turn
17[The Global Legal Entity Identifier Foundation](https://www.gleif.org/en)
18(GLEIF) is the top-level maintainer of the global registry of identifiers
19and as such has further refined the LEI format to contain the following
20components:
21
221. Characters 1-4: Prefix used to ensure the uniqueness among codes from LEI
23 issuers (Local Operating Units or LOUs).
242. Characters 5-18: Entity-specific part of the code generated and assigned by
25 LOUs according to transparent, sound and robust allocation policies. As
26 required by ISO 17442, it contains no embedded intelligence.
273. Characters 19-20: Two check digits as described in the ISO 17442 standard.
28
29GLIEF also provides daily [download files](https://www.gleif.org/en) for
30**all** registered identifiers, and an LEI search API.
31
32# Example
33
34```rust
35use codes_iso_17442::LegalEntityId;
36use std::str::FromStr;
37
38let lei = LegalEntityId::from_str("YZ83GD8L7GG84979J516").unwrap();
39
40assert_eq!(lei.local_operating_unit(), "YZ83");
41assert_eq!(lei.entity(), "GD8L7GG84979J5");
42assert_eq!(lei.check_digits(), "16");
43```
44
45# Features
46
47By default only the `serde` feature is enabled.
48
49* `serde` - Enables serialization of the [LegalEntityId] type.
50* `url` - Enables the conversion between LEI and URL (URN) forms.
51
52*/
53
54#![warn(
55 unknown_lints,
56 // ---------- Stylistic
57 absolute_paths_not_starting_with_crate,
58 elided_lifetimes_in_paths,
59 explicit_outlives_requirements,
60 macro_use_extern_crate,
61 nonstandard_style, /* group */
62 noop_method_call,
63 rust_2018_idioms,
64 single_use_lifetimes,
65 trivial_casts,
66 trivial_numeric_casts,
67 // ---------- Future
68 future_incompatible, /* group */
69 rust_2021_compatibility, /* group */
70 // ---------- Public
71 missing_debug_implementations,
72 // missing_docs,
73 unreachable_pub,
74 // ---------- Unsafe
75 unsafe_code,
76 unsafe_op_in_unsafe_fn,
77 // ---------- Unused
78 unused, /* group */
79)]
80#![deny(
81 // ---------- Public
82 exported_private_dependencies,
83 private_in_public,
84 // ---------- Deprecated
85 anonymous_parameters,
86 bare_trait_objects,
87 ellipsis_inclusive_range_patterns,
88 // ---------- Unsafe
89 deref_nullptr,
90 drop_bounds,
91 dyn_drop,
92)]
93
94use codes_agency::{standardized_type, Agency, Standard};
95use codes_check_digits::iso_7064::{get_algorithm_instance, CheckDigitAlgorithm, IsoVariant};
96use codes_check_digits::Calculator;
97use codes_common::error::{invalid_format, invalid_length};
98use codes_common::{code_as_str, code_impl, fixed_length_code, FixedLengthCode};
99use std::str::FromStr;
100
101#[cfg(feature = "serde")]
102use serde::{Deserialize, Serialize};
103
104// ------------------------------------------------------------------------------------------------
105// Public Types
106// ------------------------------------------------------------------------------------------------
107
108///
109/// An instance of the `Standard` struct defined in the
110/// [`codes_agency`](https://docs.rs/codes-agency/latest/codes_agency/)
111/// package that describes the ISO-17442 specification.
112///
113pub const ISO_17442: Standard = Standard::new_with_long_ref(
114 Agency::ISO,
115 "17442",
116 "ISO 17442-1:2020",
117 "Financial services — Legal entity identifier (LEI) — Part 1: Assignment",
118 "https://www.iso.org/standard/78829.html",
119);
120
121/// The formatted Legal Entity Identifier (LEI) is formatted as a
122/// 20-character, alpha-numeric code based on the ISO 17442 standard developed
123/// by the International Organization for Standardization (ISO). It connects
124/// to key information that enables clear and unique identification of legal
125/// entities participating in financial transactions. Each LEI database entry
126/// contains information about an entity's ownership and thus answers the
127/// questions of "who is who" and "who owns whom". Therefore the publicly
128/// available LEI data pool can be regarded as a global directory of
129/// non-individual participants in the financial market.
130///
131/// # Format
132///
133/// Note that the characters in the LEI are commonly numbered from left to
134/// right starting at one.
135///
136/// | Characters | Usage | Alphabet | Notes |
137/// | ---------- | --------------- | ------------- | --------------------- |
138/// | 1..4 | LOU ID | digits | |
139/// | 5..6 | Reserved | digits | currently always `00` |
140/// | 7..18 | Entity ID | alpha-numeric | assigned by the LOU |
141/// | 19..20 | Verification ID | digits | check digits |
142///
143/// # Examples:
144///
145/// * `5493 00 84UKLVMY22DS 16` - G.E. Financing GmbH
146/// * `2138 00 WSGIIZCXF1P5 72` - Jaguar Land Rover Ltd
147/// * `5493 00 0IBP32UQZ0KL 24` - British Broadcasting Corporation (BBC)
148///
149/// # Check Digits
150///
151/// From ISO 17442-1:2020:
152///
153/// The check digit pair shall be calculated based on the simplified procedure
154/// defined in ISO/IEC 7064 (MOD 97-10) after the conversion of the leftmost
155/// 18 alphanumeric characters into a character string consisting only of
156/// digits. The check digit pair is used to verify that the LEI code is properly
157/// formed.
158/// >
159/// > Valid check digit pairs are in the range of [02 .. 98]. 00, 01 and 99 are
160/// > not valid LEI check digit pairs.
161///
162#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
163#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
164pub struct LegalEntityId(String);
165
166pub use codes_common::CodeParseError as LegalEntityIdError;
167
168// ------------------------------------------------------------------------------------------------
169// Public Functions
170// ------------------------------------------------------------------------------------------------
171
172// ------------------------------------------------------------------------------------------------
173// Implementations
174// ------------------------------------------------------------------------------------------------
175
176const ISO_MOD_97_10: CheckDigitAlgorithm = get_algorithm_instance(IsoVariant::Mod_97_10);
177
178impl FromStr for LegalEntityId {
179 type Err = LegalEntityIdError;
180
181 fn from_str(s: &str) -> Result<Self, Self::Err> {
182 // spaces are included for clarity, see examples above.
183 let s = s.replace(' ', "");
184 if s.len() != Self::fixed_length() {
185 Err(invalid_length("LegalEntityId", s.len()))
186 } else if ISO_MOD_97_10.is_valid(&s) {
187 Ok(LegalEntityId(s))
188 } else {
189 Err(invalid_format("LegalEntityId", s))
190 }
191 }
192}
193
194#[cfg(feature = "urn")]
195impl TryFrom<url::Url> for LegalEntityId {
196 type Error = LegalEntityIdError;
197
198 fn try_from(value: url::Url) -> Result<Self, Self::Error> {
199 if !value.scheme().eq_ignore_ascii_case("urn") {
200 Err(invalid_format("LegalEntityId", value.scheme()))
201 } else {
202 let path = value.path();
203 if path[0..4].eq_ignore_ascii_case("lei:") {
204 LegalEntityId::from_str(&path[4..])
205 } else {
206 Err(invalid_format("LegalEntityId", path))
207 }
208 }
209 }
210}
211
212#[cfg(feature = "urn")]
213impl From<LegalEntityId> for url::Url {
214 fn from(v: LegalEntityId) -> url::Url {
215 url::Url::parse(&format!("urn:lei:{}", v.0)).unwrap()
216 }
217}
218
219code_impl!(LegalEntityId, as_str, str, String, to_string);
220
221code_as_str!(LegalEntityId);
222
223fixed_length_code!(LegalEntityId, 20);
224
225standardized_type!(LegalEntityId, ISO_17442);
226
227impl LegalEntityId {
228 ///
229 /// Return the portion of the LEI that corresponds to the Local Operating
230 /// Unit (LOU) accredited by GLEIF to issue entity identifiers.
231 ///
232 pub fn local_operating_unit(&self) -> &str {
233 &self.0[..4]
234 }
235
236 ///
237 /// Return the portion of the LEI that corresponds to the Entity Identifier
238 /// assigned by a GLEIF accredited Local Operating Unit (LOU).
239 ///
240 pub fn entity(&self) -> &str {
241 &self.0[4..18]
242 }
243
244 ///
245 /// Return the final two check digits from LEI.
246 ///
247 pub fn check_digits(&self) -> &str {
248 &self.0[18..]
249 }
250}
251
252// ------------------------------------------------------------------------------------------------
253// Unit Tests
254// ------------------------------------------------------------------------------------------------
255
256#[cfg(test)]
257mod tests {
258 use crate::LegalEntityId;
259 use std::str::FromStr;
260
261 #[test]
262 fn test_some_valid_lei_1() {
263 assert!(LegalEntityId::from_str("54930084UKLVMY22DS16").is_ok());
264 }
265
266 #[test]
267 fn test_some_valid_lei_2() {
268 assert!(LegalEntityId::from_str("213800WSGIIZCXF1P572").is_ok());
269 }
270
271 #[test]
272 fn test_some_valid_lei_3() {
273 assert!(LegalEntityId::from_str("5493000IBP32UQZ0KL24").is_ok());
274 }
275
276 #[test]
277 fn test_some_valid_lei_4() {
278 assert!(LegalEntityId::from_str("YZ83GD8L7GG84979J516").is_ok());
279 }
280
281 #[test]
282 fn test_some_valid_lei_components() {
283 let lei = LegalEntityId::from_str("YZ83GD8L7GG84979J516").unwrap();
284 assert_eq!(lei.local_operating_unit(), "YZ83");
285 assert_eq!(lei.entity(), "GD8L7GG84979J5");
286 assert_eq!(lei.check_digits(), "16");
287 }
288
289 #[cfg(feature = "url")]
290 #[test]
291 fn test_lei_to_url() {
292 use url::Url;
293
294 let lei = LegalEntityId::from_str("YZ83GD8L7GG84979J516").unwrap();
295 let url: Url = lei.into();
296
297 assert_eq!(url.as_str(), "urn:lei:YZ83GD8L7GG84979J516");
298 }
299
300 #[cfg(feature = "url")]
301 #[test]
302 fn test_url_to_lei() {
303 use url::Url;
304
305 let url = Url::parse("urn:lei:YZ83GD8L7GG84979J516").unwrap();
306 let lei = LegalEntityId::try_from(url).unwrap();
307
308 assert_eq!(lei.as_str(), "YZ83GD8L7GG84979J516");
309 }
310}