{"id":13631761,"url":"https://github.com/soveran/scrivener","last_synced_at":"2025-12-30T00:36:43.171Z","repository":{"id":56894547,"uuid":"2599688","full_name":"soveran/scrivener","owner":"soveran","description":"Validation frontend for models.","archived":false,"fork":false,"pushed_at":"2020-07-09T13:57:09.000Z","size":68,"stargazers_count":124,"open_issues_count":5,"forks_count":18,"subscribers_count":9,"default_branch":"master","last_synced_at":"2024-11-03T01:23:16.456Z","etag":null,"topics":["assertions","lesscode","validations"],"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/soveran.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2011-10-18T15:07:37.000Z","updated_at":"2023-12-08T00:46:51.000Z","dependencies_parsed_at":"2022-08-21T01:20:32.806Z","dependency_job_id":null,"html_url":"https://github.com/soveran/scrivener","commit_stats":null,"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soveran%2Fscrivener","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soveran%2Fscrivener/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soveran%2Fscrivener/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soveran%2Fscrivener/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/soveran","download_url":"https://codeload.github.com/soveran/scrivener/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":223768651,"owners_count":17199356,"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":["assertions","lesscode","validations"],"created_at":"2024-08-01T22:02:37.270Z","updated_at":"2025-12-30T00:36:43.139Z","avatar_url":"https://github.com/soveran.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"Scrivener\n=========\n\nValidation frontend for models.\n\nDescription\n-----------\n\nScrivener removes the validation responsibility from models and\nacts as a filter for whitelisted attributes. Read about the\n[motivation](#motivation) to understand why this separation of\nconcerns is important.\n\nUsage\n-----\n\nA very basic example would be creating a blog post:\n\n```ruby\nclass CreateBlogPost \u003c Scrivener\n  attr_accessor :title\n  attr_accessor :body\n\n  def validate\n    assert_present :title\n    assert_present :body\n  end\nend\n```\n\nIn order to use it, you have to create an instance of `CreateBlogPost`\nby passing a hash with the attributes `title` and `body` and their\ncorresponding values:\n\n```ruby\nparams = {\n  title: \"Bartleby\",\n  body: \"I am a rather elderly man...\"\n}\n\nfilter = CreateBlogPost.new(params)\n```\n\nNow you can run the validations by calling `filter.valid?`, and you\ncan retrieve the attributes by calling `filter.attributes`. If the\nvalidation fails, a hash of attributes and error codes will be\navailable by calling `filter.errors`. For example:\n\n```ruby\nif filter.valid?\n  puts filter.attributes\nelse\n  puts filter.errors\nend\n```\n\nFor now, we are just printing the attributes and the list of errors,\nbut often you will use the attributes to create an instance of a\nmodel, and you will display the error messages in a view.\n\nLet's consider the case of creating a new user:\n\n```ruby\nclass CreateUser \u003c Scrivener\n  attr_accessor :email\n  attr_accessor :password\n  attr_accessor :password_confirmation\n\n  def validate\n    assert_email :email\n\n    if assert_present :password\n      assert_equal :password, password_confirmation\n    end\n  end\nend\n```\n\nThe filter looks very similar, but as you can see the validations\nreturn booleans, thus they can be nested. In this example, we don't\nwant to bother asserting if the password and the password confirmation\nare equal if the password was not provided.\n\nLet's instantiate the filter:\n\n```ruby\nparams = {\n  email: \"info@example.com\",\n  password: \"monkey\",\n  password_confirmation: \"monkey\"\n}\n\nfilter = CreateUser.new(params)\n```\n\nIf the validation succeeds, we only need email and password to\ncreate a new user, and we can discard the password_confirmation.\nThe `filter.slice` method receives a list of attributes and returns\nthe attributes hash with any other attributes removed. In this\nexample, the hash returned by `filter.slice` will contain only the\n`email` and `password` fields:\n\n```ruby\nif filter.valid?\n  User.create(filter.slice(:email, :password))\nend\n```\n\nSometimes we might want to use parameters from the outside for validation,\nbut don't want the validator to treat them as attributes. In that case we\ncan pass arguments to `#valid?`, and they will be forwarded to `#validate`.\n\n```ruby\nclass CreateComment \u003c Scrivener\n  attr_accessor :content\n  attr_accessor :article_id\n\n  def validate(available_articles:)\n    assert_present :content\n    assert_member :article_id, available_articles.map(\u0026:id)\n  end\nend\n```\n\n```ruby\nparams = {\n  content:    \"this is a comment\",\n  article_id: 57,\n}\n\nfilter = CreateComment.new(params)\n\nfilter.valid?(available_articles: user.articles)\n```\n\nAssertions\n-----------\n\nScrivener ships with some basic assertions.\n\n### assert\n\nThe `assert` method is used by all the other assertions. It pushes the\nsecond parameter to the list of errors if the first parameter evaluates\nto `false` or `nil`.\n\n``` ruby\ndef assert(value, error)\n   value or errors[error.first].push(error.last) \u0026\u0026 false\nend\n```\n\nNew assertions can be built upon existing ones. For example, let's\ndefine an assertion for positive numbers:\n\n```ruby\ndef assert_positive(att, error = [att, :not_positive])\n  assert(send(att) \u003e 0, error)\nend\n```\n\nThis assertion calls `assert` and passes both the result of evaluating\n`send(att) \u003e 0` and the array with the attribute and the error code.\nAll assertions respect this API.\n\n### assert_present\n\nChecks that the given field is not nil or empty. The error code for\nthis assertion is `:not_present`.\n\n### assert_equal\n\nCheck that the attribute has the expected value. It uses === for\ncomparison, so type checks are possible too. Note that in order to\nmake the case equality work, the check inverts the order of the\narguments: `assert_equal :foo, Bar` is translated to the expression\n`Bar === send(:foo)`.\n\n### assert_format\n\nChecks that the given field matches the provided regular expression.\nThe error code for this assertion is `:format`.\n\n### assert_numeric\n\nChecks that the given field holds a number as a Fixnum or as a string\nrepresentation. The error code for this assertion is `:not_numeric`.\n\n### assert_url\n\nProvides a pretty general URL regular expression match. An important\npoint to make is that this assumes that the URL should start with\n`http://` or `https://`. The error code for this assertion is\n`:not_url`.\n\n### assert_email\n\nIn this current day and age, almost all web applications need to\nvalidate an email address. This pretty much matches 99% of the emails\nout there. The error code for this assertion is `:not_email`.\n\n### assert_member\n\nChecks that a given field is contained within a set of values (i.e.\nlike an `ENUM`).\n\n``` ruby\ndef validate\n  assert_member :state, %w{pending paid delivered}\nend\n```\n\nThe error code for this assertion is `:not_valid`\n\n### assert_length\n\nChecks that a given field's length falls under a specified range.\n\n``` ruby\ndef validate\n  assert_length :username, 3..20\nend\n```\n\nThe error code for this assertion is `:not_in_range`.\n\n### assert_decimal\n\nChecks that a given field looks like a number in the human sense\nof the word. Valid numbers are: 0.1, .1, 1, 1.1, 3.14159, etc.\n\nThe error code for this assertion is `:not_decimal`.\n\nMotivation\n----------\n\nA model may expose different APIs to satisfy different purposes.\nFor example, the set of validations for a User in a sign up process\nmay not be the same as the one exposed to an Admin when editing a\nuser profile. While you want the User to provide an email, a password\nand a password confirmation, you probably don't want the admin to\nmess with those attributes at all.\n\nIn a wizard, different model states ask for different validations,\nand a single set of validations for the whole process is not the\nbest solution.\n\nThis library exists to satisfy the need for extracting\n[Ohm](http://ohm.keyvalue.org)'s validations for reuse in other\nscenarios.\n\nUsing Scrivener feels very natural no matter what underlying model\nyou are using. As it provides its own validation and whitelisting\nfeatures, you can choose to ignore those that come bundled with\nORMs.\n\nSee also\n--------\n\nScrivener is [Bureaucrat](https://github.com/tizoc/bureaucrat)'s\nlittle brother. It draws all the inspiration from it and its features\nare a subset of Bureaucrat's.\n\nInstallation\n------------\n\n    $ gem install scrivener\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoveran%2Fscrivener","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsoveran%2Fscrivener","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoveran%2Fscrivener/lists"}