{"id":35216008,"url":"https://github.com/sleepingkingstudios/stannum","last_synced_at":"2026-04-09T05:32:28.098Z","repository":{"id":45127786,"uuid":"221730873","full_name":"sleepingkingstudios/stannum","owner":"sleepingkingstudios","description":"A focused library for specifying and validating data structures.","archived":false,"fork":false,"pushed_at":"2025-12-29T19:47:40.000Z","size":1343,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-02-16T05:52:40.935Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sleepingkingstudios.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2019-11-14T15:37:24.000Z","updated_at":"2025-12-29T19:45:11.000Z","dependencies_parsed_at":"2024-04-24T07:33:50.608Z","dependency_job_id":"7287e1b5-9f2e-45ef-88e7-6808f68147a1","html_url":"https://github.com/sleepingkingstudios/stannum","commit_stats":{"total_commits":181,"total_committers":1,"mean_commits":181.0,"dds":0.0,"last_synced_commit":"63b2d73e448bb528e4e0c2e468230601aaa1d34f"},"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/sleepingkingstudios/stannum","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sleepingkingstudios%2Fstannum","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sleepingkingstudios%2Fstannum/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sleepingkingstudios%2Fstannum/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sleepingkingstudios%2Fstannum/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sleepingkingstudios","download_url":"https://codeload.github.com/sleepingkingstudios/stannum/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sleepingkingstudios%2Fstannum/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31587826,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-08T14:31:17.711Z","status":"online","status_checked_at":"2026-04-09T02:00:06.848Z","response_time":112,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":[],"created_at":"2025-12-29T22:02:02.865Z","updated_at":"2026-04-09T05:32:28.080Z","avatar_url":"https://github.com/sleepingkingstudios.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Stannum\n\nA library for defining and validating data structures.\n\n\u003cblockquote\u003e\n  Read The\n  \u003ca href=\"https://www.sleepingkingstudios.com/stannum\" target=\"_blank\"\u003e\n    Documentation\n  \u003c/a\u003e\n\u003c/blockquote\u003e\n\nStannum provides a framework-independent toolkit for defining structured data entities and validations. It provides a middle ground between unstructured data (raw `Hash`es, `Structs`, or libraries like `Hashie`) and full frameworks like `ActiveModel`.\n\nIt defines the following objects:\n\n- [Constraints](http://sleepingkingstudios.github.io/stannum/constraints): A validator object that responds to `#match`, `#matches?` and `#errors_for` for a given object.\n- [Contracts](http://sleepingkingstudios.github.io/stannum/contracts): A collection of constraints about an object or its properties. Obeys the `Constraint` interface.\n- [Errors](http://sleepingkingstudios.github.io/stannum/errors): Data object for storing validation errors. Supports arbitrary nesting of errors.\n- [Entities](http://sleepingkingstudios.github.io/stannum/entities): Defines a mutable data object with a specified set of typed attributes.\n\n## Why Stannum?\n\nStannum is not tied to any framework. You can create constraints and contracts to validate Ruby objects and Entities, data structures such as Arrays, Hashes, and Sets, and even framework objects such as `ActiveRecord::Model`s and `Mongoid::Document`s.\n\nStill, most projects and applications use one framework to handle their data. Why use Stannum constraints?\n\n- **Composability:** Because Stannum contracts are their own objects, they can be combined together. Reuse validation logic without duplicating code or defining abstract ancestor classes .\n- **Polymorphism:** Your data validation is separate from your model definitions. This gives you two major advantages over the traditional approach:\n    - You can use the same contract to validate different objects. Do you have a shared concern that cuts across multiple domain objects, such as attaching images, having comments, or creating an audit trail? You can write one contract for the concern and apply that same contract to each applicable model or object.\n    - You can use different contracts to validate the same object in different contexts. Need different validations for a regular user versus an admin? Need to handle published articles more strictly than drafts? Need to provide custom validations for each step in your state machine? Stannum has you covered, and because contracts are composable, you can pull in the constraints you need without duplicating your logic.\n- **Separation of Concerns:** Your data validation is independent from your entities. This means that you can use the same tools to validate anything from controller parameters to models to configuration files.\n\n### Compatibility\n\nStannum is tested against Ruby (MRI) 3.1 through 3.4.\n\n### Documentation\n\nCode documentation is generated using [YARD](https://yardoc.org/), and can be generated locally using the `yard` gem.\n\nThe full documentation is available via [GitHub Pages](http://sleepingkingstudios.github.io/stannum), and includes the code documentation as well as a deeper explanation of Stannum's features and design philosophy. It also includes documentation for prior versions of the gem.\n\nTo generate documentation locally, see the [SleepingKingStudios::Docs](https://github.com/sleepingkingstudios/sleeping_king_studios-docs) gem.\n\n### License\n\nCopyright (c) 2019-2025 Rob Smith\n\nStannum is released under the [MIT License](https://opensource.org/licenses/MIT).\n\n### Contribute\n\nThe canonical repository for this gem is located at https://github.com/sleepingkingstudios/stannum.\n\nTo report a bug or submit a feature request, please use the [Issue Tracker](https://github.com/sleepingkingstudios/stannum/issues).\n\nTo contribute code, please fork the repository, make the desired updates, and then provide a [Pull Request](https://github.com/sleepingkingstudios/stannum/pulls). Pull requests must include appropriate tests for consideration, and all code must be properly formatted.\n\n### Code of Conduct\n\nPlease note that the `Stannum` project is released with a [Contributor Code of Conduct](https://github.com/sleepingkingstudios/stannum/blob/master/CODE_OF_CONDUCT.md). By contributing to this project, you agree to abide by its terms.\n\n## Getting Started\nLet's take a look at using Stannum to model a problem domain. Consider the following case study: we are implementing an ecommerce application. We want to ensure that our Order object is valid through each stage of our ordering process. For the sake of simplicity, let's say our process has three steps:\n\n- A customer creates an order.\n- The customer is billed for the order.\n- The order is shipped to the customer.\n\n### Defining Entities\n\nOur first step is to define some entities to represent our data.\n\n```ruby\nclass Customer\n  include Stannum::Entity\n\n  define_primary_key :id, Integer\n\n  define_attribute :email,   String\n  define_attribute :address, String\n\n  define_association :many, :orders\nend\n```\n\nOur `Customer` class represents a user who can create an order in our system. We define an `#id` primary key, attributes `#email` and `#address`, and a plural association to our `Order` entity.\n\n```ruby\nclass Payment\n  include Stannum::Entity\n\n  define_primary_key :id, Integer\n\n  define_attribute :amount, BigDecimal\n\n  define_association :one, :order, foreign_key: true\nend\n```\n\nOur `Payment` class represents a payment submitted by our customer for an order. We again define an `#id` primary key, as well as an `#amount` attribute and a singular association to an `Order`. Note that we are specifying a foreign key for the `#order` association, which automatically creates an `#order_id` attribute.\n\n```ruby\nclass Order\n  include Stannum::Entity\n\n  define_primary_key :id, Integer\n\n  define_attribute :amount, BigDecimal\n\n  define_association :one, :customer, foreign_key: true\n  define_association :one, :payment\n\n  constraint :customer, Stannum::Constraints::Presence.new\nend\n```\n\nOur core class for this workflow is the `Order` entity. We define our `#id` and `#amount`, and associations to the `Customer` and `Payment` entities - again, by passing `foreign_key: true` to our `#customer` association, we also define a `#customer_id` attribute. Finally, we are defining an additional constraint. When validating the `Order` using the default contract, we will require the `#customer` association to be populated - an `Order` must always have an associated `Customer`. However, we are *not* requiring the presence of a `Payment`, since that requirement is not applicable to the entire `Order` lifecycle.\n\n### Defining Validators\n\nActually implementing the business logic for orders is outside the scope of Stannum - for a structured approach to defining your business logic, take a look at the [Cuprum](https://www.sleepingkingstudios.com/cuprum/) gem.\n\nHowever, that doesn't mean we're finished. One of the challenges in implementing a multi-step process like our ordering flow is validation. Specifically, this kind of workflow requires *contextual* validation - an `Order` object that is valid for one part of the flow may not be valid for others. Rather than defining conditional logic in our `Order` class, let's instead apply the concept of a Validator: an object that is responsible for validating an entity or data structure *in a particular context*.\n\nLet's start with our first step, order creation. Creating an order requires a valid `#id`, a valid `#amount` (can be zero at this point in the workflow), and an associated `#customer`. Fortunately, we already have a contract defined for these requirements: the existing `Order::Contract`, which validates the entity's attributes and any additional `constraint`s defined on the entity.\n\nHere's how we could use that in our business logic:\n\n```ruby\ncustomer = Customer.new(\n  id:      0,\n  email:   'user@example.com',\n  address: '123 Example St'\n)\norder = Order.new(id: 1)\n\nOrder::Contract.matches?(order)\n#=\u003e false\nerrors = Order::Contract.errors_for(order)\n#=\u003e an instance of Stannum::Errors\nerrors.summary\n#=\u003e \"amount: is not a BigDecimal, customer: is nil or empty\"\n\norder.amount   = BigDecimal('0.0')\norder.customer = customer\n\nOrder::Contract.matches?(order)\n#=\u003e true\n```\n\nIf our contract `#matches?` the order, we proceed with the creation logic. Otherwise, we return an error message, possibly using the errors `#summary`.\n\nNow we move on to validating that an order is ready for billing. Our validation logic gets more complicated here: in addition to requiring a valid order (the same validations as above), we need to make sure that the billable amount is greater than zero.\n\nOne common approach is to add conditional validation to the `Order` class itself. For example, defining a `#status` attribute asserting that the `#amount` is greater than zero if the status matches a value. However, as new cases and conditions are added, this approach quickly becomes difficult to read and reason about. Instead, we're going to define a validator object.\n\n```ruby\nmodule Orders\n  module Contracts\n    IS_BILLABLE = Stannum::Contract.new do\n      concat(Order::Contract)\n\n      property :amount,\n        message: 'must be greater than zero',\n        type:    'orders.constraints.greater_than_zero' \\\n      do |value|\n        value.is_a?(Numeric) \u0026\u0026 value \u003e 0\n      end\n      property :payment, Stannum::Constraints::Types::NilType.new\n    end\n  end\nend\n```\n\nOur validator is an instance of `Stannum::Contract`, and our first step is to `concat` the existing `Order::Contract`. This means that all of the constraints in the `Order::Contract` will also be applied when matching an order with the `IS_BILLABLE` contract. This means we don't need to duplicate our existing constraints.\n\nSecond, we are adding a custom `constraint` on the `#amount` attribute. Notice that the first check inside the block checks that the value is `Numeric`; otherwise, comparing a `nil` value would raise an exception, rather than failing the validation. We are also defining a custom `message` and `type` for the constraint. The `message` is intended to be a human-readable representation of the error, while the `type` is intended for machines.\n\nFinally, we validate that the `#payment` association is nil, since we don't want to accidentally bill the same order twice. Here we can see why a validator object is so powerful - we obviously can't add this kind of constraint directly to `Order`, since orders later in the workflow will clearly not match. However, since our `IS_BILLABLE` contract applies only to this specific context, we can make the validation logic as specific as we want.\n\nOur final step is to ship the order to the customer. Again, to determine if the order is ready to be shipped, we define a validator object:\n\n```ruby\nmodule Orders\n  module Contracts\n    IS_SHIPPABLE = Stannum::Contract.new do\n      concat(Order::Contract)\n\n      property :payment, Stannum::Constraints::Presence.new\n\n      property :customer, Stannum::Contract.new {\n        property :address, Stannum::Constraints::Presence.new\n      }\n    end\n  end\nend\n```\n\nAgain, we define a custom `Stannum::Contract` and `concat` the existing `Order::Contract`, and we add a constraint that the `#payment` association needs to be populated - we don't want to ship an order that hasn't been paid for yet. Next, we define a nested contract to assert that the `#customer` association has a present `#address` attribute. You can define complex validation logic easily by composing together multiple constraints and contracts.\n\nHere is how we would use the `IS_BILLABLE` and `IS_SHIPPABLE` contracts in our business logic:\n\n```ruby\ncustomer = Customer.new(\n  id:      0,\n  email:   'user@example.com',\n  address: '123 Example St'\n)\norder = Order.new(id: 1, customer:)\n\nOrders::Contracts::IS_BILLABLE.matches?(order)\n#=\u003e false\nerrors = Orders::Contracts::IS_BILLABLE.errors_for(order)\nerrors.summary\n#=\u003e \"amount: is not a BigDecimal, amount: must be greater than zero\"\n\norder.amount = BigDecimal('100.0')\nOrders::Contracts::IS_BILLABLE.matches?(order)\n#=\u003e true\n\nOrders::Contracts::IS_SHIPPABLE.matches?(order)\n#=\u003e false\nerrors = Orders::Contracts::IS_SHIPPABLE.errors_for(order)\nerrors.summary\n#=\u003e \"payment: is nil or empty\"\n\norder.payment = Payment.new(id: 2, amount: order.amount)\nOrders::Contracts::IS_SHIPPABLE.matches?(order)\n#=\u003e true\n```\n\nAs we define our `BillOrder` and `ShipOrder` classes, we will be able to use our `IS_BILLABLE` and `IS_SHIPPABLE` contracts to quickly identify orders that are invalid for that context.\n\n### Validating Other Data\n\nIn addition to using them with entities, `Stannum` constraints and contracts can be used to validate almost any sort of data. For example, consider our ordering workflow. Perhaps we make a API call to a company that actually ships the order to the customer. We want to ensure that the API response contains the expected data. We can define a contract to validate the returned JSON body:\n\n```ruby\nSUCCESS_RESPONSE_CONTRACT = Stannum::Contract.new do\n  property :status, Stannum::Constraints::Identity.new(200)\n\n  property :body, Stannum::Contracts::HashContract.new {\n    key 'ok', Stannum::Constraints::Identity.new(true)\n\n    key 'shipping_confirmation',\n      Stannum::Constraint.new(\n        message: 'be a string with length 24',\n        type:    'orders.constraints.valid_shipping_confirmation'\n      ) { |value|\n        value.is_a?(String) \u0026\u0026 value.size == 24\n      }\n  }\nend\n```\n\nWe can then validate the API response by calling `SUCCESS_RESPONSE_CONTRACT.matches?(response)`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsleepingkingstudios%2Fstannum","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsleepingkingstudios%2Fstannum","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsleepingkingstudios%2Fstannum/lists"}