{"id":13995218,"url":"https://github.com/slowtec/semval","last_synced_at":"2026-04-06T06:03:19.968Z","repository":{"id":57666583,"uuid":"201768291","full_name":"slowtec/semval","owner":"slowtec","description":"Semantic validation for Rust","archived":false,"fork":false,"pushed_at":"2024-02-12T20:17:44.000Z","size":143,"stargazers_count":86,"open_issues_count":1,"forks_count":4,"subscribers_count":3,"default_branch":"main","last_synced_at":"2024-08-10T14:18:39.726Z","etag":null,"topics":["no-std","rust","validation"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/slowtec.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSES/CC0-1.0.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2019-08-11T13:29:26.000Z","updated_at":"2024-07-23T20:46:18.000Z","dependencies_parsed_at":"2024-01-03T21:23:44.526Z","dependency_job_id":"f9f40242-cbc3-483f-913e-6e41ade6b8f2","html_url":"https://github.com/slowtec/semval","commit_stats":{"total_commits":138,"total_committers":3,"mean_commits":46.0,"dds":0.3913043478260869,"last_synced_commit":"9912044cce62f2876cbce21f672e4f93a56b0c2e"},"previous_names":[],"tags_count":23,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/slowtec%2Fsemval","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/slowtec%2Fsemval/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/slowtec%2Fsemval/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/slowtec%2Fsemval/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/slowtec","download_url":"https://codeload.github.com/slowtec/semval/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":227177816,"owners_count":17743171,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["no-std","rust","validation"],"created_at":"2024-08-09T14:03:18.610Z","updated_at":"2025-12-12T14:03:21.890Z","avatar_url":"https://github.com/slowtec.png","language":"Rust","funding_links":[],"categories":["Rust"],"sub_categories":[],"readme":"\u003c!-- SPDX-FileCopyrightText: slowtec GmbH --\u003e\n\u003c!-- SPDX-License-Identifier: MPL-2.0 --\u003e\n\n# semval\n\n[![Crates.io](https://img.shields.io/crates/v/semval.svg)](https://crates.io/crates/semval)\n[![Docs.rs](https://docs.rs/semval/badge.svg)](https://docs.rs/semval)\n[![Deps.rs](https://deps.rs/repo/github/slowtec/semval/status.svg)](https://deps.rs/repo/github/slowtec/semval)\n[![Security audit](https://github.com/slowtec/semval/actions/workflows/security-audit.yaml/badge.svg)](https://github.com/slowtec/semval/actions/workflows/security-audit.yaml)\n[![Continuous integration](https://github.com/slowtec/semval/actions/workflows/build-and-test.yaml/badge.svg)](https://github.com/slowtec/semval/actions/workflows/build-and-test.yaml)\n[![License: MPL 2.0](https://img.shields.io/badge/License-MPL_2.0-brightgreen.svg)](https://opensource.org/licenses/MPL-2.0)\n\nA lightweight and unopinionated library with minimal dependencies for semantic validation in Rust.\n\nWithout any macro magic, at least not now.\n\nTL;DR If you need to validate complex data structures at runtime then this crate may empower you to\nenrich your domain model with semantic validation.\n\n## Motivation\n\nHow do you recursively validate complex data structures, collect all violations along the way, and\nfinally report or evaluate those findings? Validating external data at runtime before feeding it\ninto further processing stages is crucial to avoid inconsistencies and even more to prevent physical\ndamage.\n\n## Example\n\n### Use case\n\nAssume you are creating a web service for managing _reservations_ in a restaurant. Customers can\nplace reservations for a certain start time and a number of guests. As _contact data_ they need to\nleave their _phone number_ or _e-mail address_, at least one of both.\n\nThe JSON request body for creating a new reservation may look like in this example:\n\n```json\n{\n  \"start\": \"2019-07-30T18:00:00Z\",\n  \"number_of_guests\": 4,\n  \"customer\": {\n    \"name\": \"slowtec GmbH\",\n    \"contact_data\": {\n      \"phone\": \"+49 711 500 716 72\",\n      \"email\": \"post@slowtec.de\"\n    }\n  }\n}\n```\n\n### Domain model\n\nLet's focus on the contact data. The corresponding type-safe data model in Rust might look like\nthis:\n\n```rust\nstruct PhoneNumber(String);\n\nstruct EmailAddress(String);\n\nstruct ContactData {\n  pub phone: Option\u003cPhoneNumber\u003e,\n  pub email: Option\u003cEmailAddress\u003e,\n}\n```\n\nIn this example, both phone number and e-mail address are still represented by strings, but wrapped\ninto [tuple structs](https://doc.rust-lang.org/1.9.0/book/structs.html#tuple-structs) with a single\nmember. This commonly used\n[_newtype_ pattern](https://doc.rust-lang.org/book/ch19-04-advanced-types.html?highlight=newtype#using-the-newtype-pattern-for-type-safety-and-abstraction)\nestablishes type safety at compile time and enables us to add _behavior_ to these types.\n\n### Business Rules\n\nOur reservation business requires that contact data entities are only accepted if all of the\nfollowing conditions are satisfied:\n\n- The e-mail address is valid\n- The phone number is valid\n- Either e-mail address, or phone number, or both are present\n\n## Validation\n\nLet's develop a software design for the _reservation_ example use case. It should empower us to\nvalidate domain entities according to our business requirements.\n\nWe will solely focus on the _contact data_ entity for simplicity. This is sufficient to deduce the\nbasic principles. The complete code can be found in the file\n[reservation.rs](https://github.com/slowtec/semval/blob/main/examples/reservation.rs) that is\nprovided as an example in the repository.\n\n### Invalidity\n\nWhat are the possible outcomes of a validation? If the validation succeeds we are done and\nprocessing continues as if nothing happened, i.e. validation is typically an\n[_idempotent_](https://en.wikipedia.org/wiki/Idempotence) operation. If the validation fails we\nsomehow want to understand why it failed to resolve conflicts or to fix inconsistencies. Finally, we\nmay need to report any unresolved findings back to the caller.\n\nReasons for a failed validation are expressed in terms of _invalidity_. An invalidity is basically\nthe inverse of some validation condition.\n\nThe invalidity variants for contact data are:\n\n- The e-mail address is invalid\n- The phone number is invalid\n- Both e-mail address and phone number are missing\n\nPlease note that different invalidity variants may apply at the same time, e.g. both e-mail address\nand phone number might be invalid for the same entity.\n\n### Results\n\nWe already realized that the successful result of a validation is essentially _nothing_. In Rust\nthis nothing is represented by the unit type `()`.\n\nAny invalidity will cause the validation to fail. Does this mean we should fail early and abort the\nvalidation when detecting the first invalidity? Not necessarily. Consider the use case of form\nvalidation with direct user interaction. If the user submits a form with multiple invalid or missing\nfields we should report all of them to reduce the number of unsuccessful retries and round trips.\n\nThis leads us to a preliminary definition for validation results:\n\n```rust\ntype NaiveValidationResult = Result\u003c(), Vec\u003cInvalidity\u003e\u003e\n```\n\nWe will refine it in a moment.\n\n### Context\n\nValidation is a recursive operation that needs to traverse deeply nested data structures. The\ncurrent state during such a traversal defines a _context_ for the validation with a certain _level\nof abstraction_.\n\nAt the `ContactData` level we need to recursively validate both phone number and e-mail address if\npresent. Those subordinate validations are performed on a lower level of abstraction, unaware of the\nupper-level context.\n\nAdditionally, we check if both members are missing and then reject the `ContactData` as\n_incomplete_. This is the only validation that is actually implemented on the current level without\nrecursion.\n\nLet's encode all possible variants in Rust by using _sum types_:\n\n```rust\nenum PhoneNumberInvalidity {\n  ...lower abstraction level...\n}\n\nenum EmailAddressInvalidity {\n  ...lower abstraction level...\n}\n\nenum ContactDataInvalidity {\n  Phone(PhoneNumberInvalidity),\n  Email(EmailAddressInvalidity),\n  Incomplete,\n}\n```\n\nPlease note that each validation result refers to only a single `Invalidity` type. The recursive\nnesting of validation results from lower-level contexts is achieved by wrapping their `Invalidity`\ntypes into subordinate variants. The names of those variants typically resemble the role names\nwithin the current context.\n\n### Results ...continued\n\nWith the preliminary considerations, we are now able to finalize our definition of a generic\nvalidation result:\n\n```rust\nstruct ValidationContext\u003cV: Invalidity\u003e {\n  ...implementation details...\n}\n\ntype ValidationResult\u003cV: Invalidity\u003e = Result\u003c(), ValidationContext\u003cV\u003e\u003e\n```\n\nThe `ValidationContext` is responsible for collecting validation results in the form of multiple\nvariants of the associated `Invalidity` type. Each item represents a violation of some validation\ncondition, i.e. a single invalidity that has been detected. The concrete implementation of how\ninvalidities are collected is hidden.\n\n### Behavior\n\nWe enhance our domain entities by implementing the generic `Validate` trait:\n\n```rust\npub trait Validate {\n    type Invalidity: Invalidity;\n\n    fn validate(\u0026self) -\u003e ValidationResult\u003cSelf::Invalidity\u003e;\n}\n```\n\nThe associated type `Invalidity` is typically defined as a companion type of the corresponding\ndomain entity, as we have seen above. Don't get confused by the trait bound of the same name that is\njust an alias for `Any + Debug`.\n\nProvided that all components of our composite entity `ContactData` already implement this trait the\nimplementation becomes straightforward:\n\n```rust\nimpl Validate for ContactData {\n    type Invalidity = ContactDataInvalidity;\n\n    fn validate(\u0026self) -\u003e ValidationResult\u003cSelf::Invalidity\u003e {\n        ValidationContext::new()\n            .validate_with(\u0026self.email, ContactDataInvalidity::EmailAddress)\n            .validate_with(\u0026self.phone, ContactDataInvalidity::PhoneNumber)\n            .invalidate_if(\n                // Either email or phone must be present\n                self.email.is_none() \u0026\u0026 self.phone.is_none(),\n                ContactDataInvalidity::Incomplete,\n            )\n            .into()\n    }\n}\n```\n\nThe validation function starts by creating a new, empty context. Then it continues by recursively\ncollecting results from subordinate validations as well as executing own validations rules. Finally,\nit transforms the context into a result for passing it back to the caller.\n\nThe [_fluent interface_](https://martinfowler.com/bliki/FluentInterface.html) has proven to be\nuseful and readable for the majority of use cases, even if more complex validations may require to\nbreak the control flow at certain points.\n\n## Corollary\n\nWe have translated the validation rules for our business requirements into a few lines of\ncomprehensive code. This code is associated with the corresponding domain entity and only needs to\nconsider a single level of abstraction. Recursive composition enables us to validate complex data\nstructures and to trace back the cause of failed validations.\n\nThe validation code is independent of infrastructure components and an ideal candidate for including\nit in the\n[_functional core_](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell)\nof a system. With simple unit tests we can verify that the validation works as expected and reliably\nprotects us from accepting invalid data.\n\n## What not\n\nWe didn't cover\n\n- how to enhance `Invalidity` types with additional, context-sensitive data by defining them as\n  _tagged variants_ and\n- how to route and interpret validation results.\n\nThe answers to both questions depend on each other, require use case-specific solutions, and are not\nrestricted by this library in any way.\n\n## License\n\nLicensed under the Mozilla Public License 2.0 (MPL-2.0) (see [MPL-2.0.txt](LICENSES/MPL-2.0.txt) or\n\u003chttps://www.mozilla.org/MPL/2.0/\u003e).\n\nPermissions of this copyleft license are conditioned on making available source code of licensed\nfiles and modifications of those files under the same license (or in certain cases, one of the GNU\nlicenses). Copyright and license notices must be preserved. Contributors provide an express grant of\npatent rights. However, a larger work using the licensed work may be distributed under different\nterms and without source code for files added in the larger work.\n\n### Contribution\n\nAny contribution intentionally submitted for inclusion in the work by you shall be licensed under\nthe Mozilla Public License 2.0 (MPL-2.0).\n\nIt is required to add the following header with the corresponding\n[SPDX short identifier](https://spdx.dev/ids/) to the top of each file:\n\n```rust\n// SPDX-License-Identifier: MPL-2.0\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fslowtec%2Fsemval","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fslowtec%2Fsemval","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fslowtec%2Fsemval/lists"}