{"id":16746800,"url":"https://github.com/goltergaul/definition","last_synced_at":"2025-11-11T20:04:24.304Z","repository":{"id":18920237,"uuid":"85576860","full_name":"Goltergaul/definition","owner":"Goltergaul","description":"Simple and composable validation and coercion of data structures","archived":false,"fork":false,"pushed_at":"2025-04-22T04:52:17.000Z","size":233,"stargazers_count":18,"open_issues_count":1,"forks_count":2,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-10-27T15:59:39.284Z","etag":null,"topics":["coercion","ruby","validation","validation-library","value-object"],"latest_commit_sha":null,"homepage":"","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/Goltergaul.png","metadata":{"files":{"readme":"README.md","changelog":"Changelog.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"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}},"created_at":"2017-03-20T12:57:50.000Z","updated_at":"2025-06-26T23:48:12.000Z","dependencies_parsed_at":"2023-11-22T16:02:14.242Z","dependency_job_id":"f718eb39-f843-4fe6-8bd0-ca30e3b2398f","html_url":"https://github.com/Goltergaul/definition","commit_stats":{"total_commits":76,"total_committers":6,"mean_commits":"12.666666666666666","dds":0.368421052631579,"last_synced_commit":"963748a99e39b6f2f93e428b5690e2d42796561d"},"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/Goltergaul/definition","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Goltergaul%2Fdefinition","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Goltergaul%2Fdefinition/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Goltergaul%2Fdefinition/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Goltergaul%2Fdefinition/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Goltergaul","download_url":"https://codeload.github.com/Goltergaul/definition/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Goltergaul%2Fdefinition/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":283921118,"owners_count":26916743,"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","status":"online","status_checked_at":"2025-11-11T02:00:06.610Z","response_time":65,"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":["coercion","ruby","validation","validation-library","value-object"],"created_at":"2024-10-13T02:08:22.806Z","updated_at":"2025-11-11T20:04:24.260Z","avatar_url":"https://github.com/Goltergaul.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Definition\n\n[![Gem Version](https://badge.fury.io/rb/definition.svg)][rubygems]\n\nSimple and composable validation and coercion of data structures. It also includes a ValueObject for convenience.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'definition'\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install definition\n\n## Usage\n\nDefinitions can be used to validate data structures like for example Hashes:\n\n```ruby\nschema = Definition.Keys do\n  required :first_name, Definition.Type(String)\n  required :last_name, Definition.Type(String)\n  optional :birthday, Definition.Type(Date)\nend\n\nconform_result = schema.conform({first_name: \"John\", last_name: \"Doe\", birthday: Date.today})\nconform_result.passed? # =\u003e true\n\nconform_result = schema.conform({first_name: \"John\", last_name: \"Doe\", birthday: \"2018/02/09\"})\nconform_result.passed? # =\u003e false\nconform_result.error_message # =\u003e hash fails validation for key birthday: { Is of type String instead of Date }\nconform_result.error_hash # =\u003e\n# {\n#     :birthday =\u003e [\n#         [0] \u003cDefinition::ConformError\n#               description: \"hash fails validation for key birthday: { Is of type String instead of Date }\",\n#               json_pointer: \"/birthday\"\u003e\n#     ]\n# }\n```\n\nBut it can also transform those data structures at the same time. The following\nexample shows how a Unix timestamp in milliseconds can be transformed to a Time\nobject while validating:\n\n```ruby\nmilliseconds_time_definition = Definition.Lambda(:milliseconds_time) do |value|\n  conform_with(Time.at(value.to_r / 1000).utc) if value.is_a?(Integer)\nend\n\nschema = Definition.Keys do\n  required :title, Definition.Type(String)\n  required :body, Definition.Type(String)\n  optional :publication_date, milliseconds_time_definition\nend\n\nconform_result = schema.conform({title: \"My first blog post\", body: \"Shortest one ever!\", publication_date: 1546170180339})\nconform_result.passed? # =\u003e true\nconform_result.value # =\u003e {title: \"My first blog post\", body: \"Shortest one ever!\", publication_date: 2018-12-30 11:43:00 UTC}\n```\n\nBecause definitions do not only validate input but also transform input, we use\nthe term `conform` which stands for validation and coercion.\n\n### Handling errors\n\n#### I18n translated errors\nFor end users you best use the translated errors that you get from definition:\n\n```ruby\nschema = Definition.Keys do\n  required :title, Definition.NonEmptyString\n  required :body, Definition::And(\n                    Definition.Type(String),\n                    Definition.MinSize(100)\n                  )\nend\n\nconform_result = schema.conform({title: \"\", body: \"this is not long enough\"})\nconform_result.errors # =\u003e returns an array of Definition::ConformError\nconform_result.errors.each do |error|\n  puts \"----\"\n  puts error.json_pointer # provides a path to the invalid value, also works with nested objects and arrays\n  puts error.translated_error\nend\n# =\u003e\n# ----\n# /title\n# Value is shorter than 1\n# ----\n# /body\n# Value is shorter than 100\n```\n\nThe error messages are only translated into English for now, but you can add or change translations by adding a yaml file like [this](./config/locales/en.yml) to your I18n load path.\n\n#### Other ways of accessing errors\n\nTo get a quick error summary during debugging, you can also use `conform_result.error_message`\n\nInstead of getting a flat array of all errors via `conform_result.errors`, you can also get a hierarchical representation:\n\n```ruby\nconform_result.error_hash\n# =\u003e\n# {\n#     :title =\u003e [\n#         [0] \u003cDefinition::ConformError \n# \t message: \"hash fails validation for key title: { Not all definitions are valid for 'non_empty_string': { Did not pass test for min_size (1) } }\", \n# \t json_pointer: \"/title\"\u003e\n#     ],\n#      :body =\u003e [\n#         [0] \u003cDefinition::ConformError \n# \t message: \"hash fails validation for key body: { Not all definitions are valid for 'and': { Did not pass test for min_size (100) } }\", \n# \t json_pointer: \"/body\"\u003e\n#     ]\n# }\n\n```\n\n### Conforming Hashes\n\nHashes can be conformed by using the `Keys` definition. It allows you to configure\nrequired and optional attributes. The first argument of `required` and `optional`\ntakes either Symbols or Strings. If you use a Symbol, then the validated Hash\nneeds to have a Symbol key with that name, otherwise a string key.\n\nThe key definition will also fail if the input value contains extra keys.\n\nYou can configure default values for optional keys, see the following example.\n\n```ruby\nDefinition.Keys do\n  required :title, Definition.NonEmptyString\n  optional :publication_date, Definition.Type(Date)\n  optional :is_draft, Definition.Boolean, default: true\nend\n```\n\n#### Ignoring unexpected keys\n\nBy default the `Keys` Definition does not conform with input hashes that contains\nkeys that are not defined in the Definition. You can set the `:ignore_extra_keys`\noption to disable this.\n\n```ruby\nschema = Definition.Keys do\n  option :ignore_extra_keys\n\n  required :title, Definition.NonEmptyString\n  optional :publication_date, Definition.Type(Time)\nend\n\nconform_result = schema.conform({title: \"My first blog post\", body: \"Shortest one ever!\", publication_date: Time.new})\nconform_result.passed? # =\u003e true\nconform_result.value # =\u003e {title: \"My first blog post\", publication_date: 2018-12-30 11:43:00 UTC}\n```\n\n### Validating types\n\nThis will validate that the value is of the specified type.\n\n```ruby\nDefinition.Type(String)\nDefinition.Type(Float)\nDefinition.Type(MyClass)\n\nDefinition.Type(MyClass).conform(0.1).passed? # =\u003e false\nDefinition.Type(MyClass).conform(MyClass.new).passed? # =\u003e true\n```\n\n### Conforming types\n\nThis will validate that the value is of the specified type. But if its not it will\ntry to coerce it into that type. This Definition works only with primitive types.\n\n```ruby\nDefinition.CoercibleType(String) # Uses String() to coerce values\nDefinition.CoercibleType(Float) # Uses Float() to coerce values\n\nDefinition.CoercibleType(Float).conform(\"0.1\").passed? # =\u003e true\nDefinition.CoercibleType(Float).conform(\"0.1\").value # =\u003e 0.1\n```\n\n### Combining multiple definitions with \"And\"\n\n```ruby\nDefinition.And(definition1, definition2, ...)\n```\n\nThis definition will only conform if all definitions conform. The definitions will\nbe processed from left to right and the output of the previous will be the input\nof the next. Processing of the And-Definition stops as soon as one definition does not conform.\n\n### Combining multiple definitions with \"Or\"\n\n```ruby\nDefinition.Or(definition1, definition2, ...)\n```\n\nThis definition will conform if at least one definition conforms. The definitions will\nbe processed from left to right and stop as soon as a definition conforms. The output\nof that definition will be the output of the Or definition.\n\n### Conforming array values with \"Each\"\n\n```ruby\nDefinition.Each(item_definition)\n\nDefinition.Each(Definition.Type(Integer)).conform([1,2,3,\"4\"]).error_message\n# =\u003e Not all items conform with each: { Item \"4\" did not conform to each: { Is of type String instead of Integer } }\n```\n\nThis definition will only conform if all elements of the value conform to the\n`item_definition`.\n\n### Conforming with custom lambda functions\n\n```ruby\nDefinition.Lambda(:password) do |value|\n  matches = Regexp.new(/^\n    (?=.*[a-z]) # should contain at least one lower case letter\n    (?=.*[A-Z]) # should contain at least one upper case letter\n    (?=.*\\d)    # should contain at least one digit\n    .{6,50}     # should be between 6 and 50 characters long\n    $/x).match(value.to_s)\n  conform_with(value) if matches\nend\n```\n\nThis definition can be used to build any custom validation or coercion you want.\nThe example above makes sure that a password conforms with a set of rules.\n\nThe block gets the input value as argument and you can do any transformation or\nvalidation on it that you want. If you determine that the value is valid, then\nyou must call `conform_with` and pass it the value you want to return. This can\neither be the original value or any transformed version of it. By not calling\n`conform_with` you tell the definition to fail for the current input value.\n\nThe first argument of `Definition.Lambda` is a name you can give this definition.\nIt will only be used in the error message to make it more readable.\n\nIf you want to provide detailed custom error messages you can use `fail_with`:\n\n```ruby\nDefinition.Lambda(:password) do |value|\n  if !value.match(/[a-z]+/)\n    fail_with(\"must contain at least one lower case letter\")\n  elsif !value.match(/[A-Z]+/)\n    fail_with(\"must contain at least one upper case letter\") \n  elsif !value.match(/\\d+/)\n    fail_with(\"must contain at least one digit\") \n  elsif value.size \u003c 6 || value.size \u003e 50\n    fail_with(\"must be between 6 and 50 characters long\") \n  else\n    conform_with(value)\n  end\nend\n```\n\n### Composing Definitions\n\nDefinitions are reusable and can be easily composed:\n\n```ruby\ncountry_code_definition = Definition.Lambda(:iso_county_code) do |value|\n  if iso_code = IsoCountryCodes.find(value)\n    conform_with(iso_code.alpha2)\n  end\nend\n\naddress_definition = Definition.Keys do\n  required :street, Definition.Type(String)\n  required :postal_code, Definition.Type(String)\n  required :country_code, country_code_definition\nend\n\norder = Definition.Keys do\n  required :user, user_definition\n  required :invoice_address, address_definition\n  required :shipping_address, address_definition\nend\n```\n\n### Extending Key definitions with include\n\nBesides composing Definitions, you can also include `Keys` Definitions in each \nother. This will basically copy all required and optional keys as well as defaults into the other definition.\n\n```ruby\naddress_definition = Definition.Keys do\n  required :street, Definition.Type(String)\n  required :postal_code, Definition.Type(String)\n  required :country_code, Definition.Type(String)\nend\n\nuser_definition = Definition.Keys do\n  required :user, user_definition\n\n  include address_definition\nend\n```\nAbove Definition will equal the following:\n```ruby\nuser_definition = Definition.Keys do\n  required :user, user_definition\n\n  required :street, Definition.Type(String)\n  required :postal_code, Definition.Type(String)\n  required :country_code, Definition.Type(String)\nend\n```\n\n### Predefined Definitions\n\n#### Strings and Arrays\n\n```ruby\nDefinition.MaxSize(5).conform(\"house\") # =\u003e pass\nDefinition.MaxSize(5).conform([1,2,3,4,5]) # =\u003e pass\n```\n\n```ruby\nDefinition.MinSize(5).conform(\"house\") # =\u003e pass\nDefinition.MinSize(5).conform([1,2,3,4,5]) # =\u003e pass\n```\n\n#### Strings\n\n```ruby\nDefinition.NonEmptyString.conform(\"house\") # =\u003e pass\n```\n\n```ruby\nDefinition.Regex(/^\\d*$/).conform(\"123\") # =\u003e pass\n```\n\n#### Numerics\n\n```ruby\nDefinition.GreaterThan(5).conform(5.1) # =\u003e pass\nDefinition.GreaterThanEqual(5).conform(5) # =\u003e pass\nDefinition.LessThan(5).conform(4) # =\u003e pass\nDefinition.LessThanEqual(5).conform(5) # =\u003e pass\n```\n\n#### Strings, Array, Hashes\n\n```ruby\nDefinition.Empty.conform(\"\") # =\u003e pass\nDefinition.Empty.conform([]) # =\u003e pass\nDefinition.Empty.conform({}) # =\u003e pass\n```\n\n```ruby\nDefinition.NonEmpty.conform(\"Joe\") # =\u003e pass\nDefinition.NonEmpty.conform([1]) # =\u003e pass\nDefinition.NonEmpty.conform({ a: 1 }) # =\u003e pass\n```\n\n#### Nil\n\n```ruby\nDefinition.Nil.conform(nil) # =\u003e pass\n```\n\n#### Boolean\n\n```ruby\nDefinition.Boolean.conform(true) # =\u003e pass\n```\n\n#### All types\n\n```ruby\nDefinition.Equal(5).conform(5) # =\u003e pass\nDefinition.Equal(\"foo\").conform(\"foo\") # =\u003e pass\n```\n\nThe Nilable Definition allows a value to be nil or to conform\nwith the definition you pass it as argument:\n\n```ruby\nDefinition.Nilable(Definition.Type(String)).conform(nil) # =\u003e pass\nDefinition.Nilable(Definition.Type(String)).conform(\"foo\") # =\u003e pass\n```\n\nThe Enum Definition checks if the input equals one of the values you pass it as argument. You can pass in as many arguments as you like:\n\n```ruby\nDefinition.Enum(\"foo\", 1, 2.0).conform(\"foo\") # =\u003e pass\nDefinition.Enum(\"foo\", 1, 2.0).conform(1) # =\u003e pass\nDefinition.Enum(\"foo\", 1, 2.0).conform(\"bar) # =\u003e fail\n```\n\n### Examples\n\nCheck out the [integration specs](./spec/integration) for more usage examples.\n\n### I18n translations\n\nEvery error object has a method `translated_error` that will give you a translated\nversion of the error message. You can load the default English translations shipped\nwith the gem by adding them to your I18n load path.\n\n\n```ruby\nschema = Definition.Keys do\n  required :title, Definition.Type(String)\n  required :body, Definition.Type(String)\n  required(:author, Definition.Keys do\n    required :name, Definition.Type(String)\n    required :email, Definition.Type(String)\n  end)\nend\nschema.conform(input_hash).errors.first.translated_error # =\u003e Value is of wrong type, needs to be a String\"\n```\n\n# Helpers / useful tools\n\n### Value Objects / Models\n\nProvides simple immutable objects that can validate and hold your data so that it can be safely passed around in your application.\n\n```ruby\nclass User \u003c Definition::Model\n  required :username, Definition.Type(String)\n  required :password, Definition.Type(String)\n  optional :age, Definition.Type(Integer)\nend\n\nuser = User.new(username: \"johndoe\", password: \"zg(2ds8x2/\")\nuser.username # =\u003e \"johndoe\"\nuser.age # =\u003e nil\nuser.to_h # =\u003e { username: \"johndoe\", password: \"zg(2ds8x2/\" }\nuser.new(age: 21) # =\u003e new model instance with username and password from before plus the age set to 21\n\nuser.username = \"Alice\" # =\u003e raises NoMethodError (Models are immutable)\n\nUser.new(username: \"johndoe\") # =\u003e raises a Definition::InvalidModelError: hash is missing :password\n\n```\n\nYou can access the conform result of InvalidModel errors via their `conform_result` method.\n\n#### Nesting Models\n\nModels can be nested by either using the model object itself as type definition,\nor by using the `CoercibleModel` Definition. The latter is less strict and will \nconvert input hashes that conform with the model schema to an instance of the model.\n\n```ruby\nclass Address \u003c Definition::Model\n  required :street, Definition.Type(String)\n  required :postal_code, Definition.Type(String)\nend\n\nclass User \u003c Definition::Model\n  required :username, Definition.Type(String)\n  required :address, Definition.CoercibleModel(Address)\nend\n\n# Address is converted into an Address model automatically:\nuser = User.new(username: \"John\", address: { street: \"123 Fakestreet\", postal_code: \"2dfx4\" })\nuser.address.street # =\u003e \"123 Fakestreet\"\n\nclass UserNotCoercibleAddress \u003c Definition::Model\n  required :username, Definition.Type(String)\n  required :address, Address\nend\n\n# Address is not converted automatically, instead it needs to be of type Address already:\nUserNotCoercibleAddress.new(username: \"John\", address: { street: \"123 Fakestreet\", postal_code: \"2dfx4\" }) # =\u003e raises a Definition::InvalidModelError\nUserNotCoercibleAddress.new(username: \"John\", address: Address.new(street: \"123 Fakestreet\", postal_code: \"2dfx4\")).address.street # =\u003e \"123 Fakestreet\"\n```\n\n### Intialization argument validation\n\nDefinition provides a mixin that allows you to validate keyword arguments of class initialization methods. This is meant to be used with classes that provide business logic whereas the models are meant to be used to pass data around.\n\nThe major differences to a Definition::Model are:\n* The values are not frozen and can be changed by the classes internal business logic\n* None of the getters for the attributes are public\n\n```ruby\nclass User\n  include Definition::Initializer\n\n  required :id, Definition.Type(Integer)\n  required :name, Definition.Type(String)\n  optional :phone, Definition.Type(String), default: nil\n\n  def hello\n    puts \"Hello, I'm #{name}\"\n  end\nend\n\nuser = User.new(id: 1, name: \"Joe\")\nuser.hello # =\u003e \"Hello, I'm Joe\"\nuser.name # =\u003e raises NoMethodError\n\nUser.new(id: \"1\", name: \"Joe\") # =\u003e raises a Definition::Initializer::InvalidArgumentError\n```\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.\n\nTo install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/Goltergaul/definition.\n\n[circleci]: https://circleci.com/gh/Goltergaul/definition\n[rubygems]: https://rubygems.org/gems/definition\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgoltergaul%2Fdefinition","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgoltergaul%2Fdefinition","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgoltergaul%2Fdefinition/lists"}