{"id":13682787,"url":"https://github.com/krzemin/octopus","last_synced_at":"2025-04-09T13:06:14.922Z","repository":{"id":14186593,"uuid":"76125361","full_name":"krzemin/octopus","owner":"krzemin","description":"Scala library for boilerplate-free validation","archived":false,"fork":false,"pushed_at":"2024-06-24T17:37:16.000Z","size":154,"stargazers_count":150,"open_issues_count":23,"forks_count":23,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-04-02T04:59:56.621Z","etag":null,"topics":["boilerplate","cats","scala","scalaz","shapeless","validation"],"latest_commit_sha":null,"homepage":null,"language":"Scala","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/krzemin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","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},"funding":{"github":"krzemin"}},"created_at":"2016-12-10T16:51:09.000Z","updated_at":"2025-02-23T10:32:36.000Z","dependencies_parsed_at":"2024-08-02T13:22:46.600Z","dependency_job_id":"5edd3861-943d-46d2-a340-d50a2db7a384","html_url":"https://github.com/krzemin/octopus","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/krzemin%2Foctopus","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/krzemin%2Foctopus/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/krzemin%2Foctopus/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/krzemin%2Foctopus/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/krzemin","download_url":"https://codeload.github.com/krzemin/octopus/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248045231,"owners_count":21038553,"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":["boilerplate","cats","scala","scalaz","shapeless","validation"],"created_at":"2024-08-02T13:01:53.158Z","updated_at":"2025-04-09T13:06:14.907Z","avatar_url":"https://github.com/krzemin.png","language":"Scala","funding_links":["https://github.com/sponsors/krzemin"],"categories":["Scala","Table of Contents","Data Binding and Validation"],"sub_categories":["Data Binding and Validation"],"readme":"# Octopus\n\n[![CI build](https://github.com/krzemin/octopus/workflows/CI%20build/badge.svg)](https://github.com/krzemin/octopus/actions?query=workflow%3A%22CI+build%22)\n[![Maven Central](https://img.shields.io/maven-central/v/com.github.krzemin/octopus_2.13.svg)](http://search.maven.org/#search%7Cga%7C1%7Coctopus)\n[![codecov.io](http://codecov.io/github/krzemin/octopus/coverage.svg?branch=master)](http://codecov.io/github/krzemin/octopus?branch=master)\n[![License](http://img.shields.io/:license-Apache%202-green.svg)](http://www.apache.org/licenses/LICENSE-2.0.txt)\n\nOctopus is a Scala library for boilerplate-free validation.\n\nIt defines `Validator[T]` type-class, provide composable DSL for\ndefining validation rules for user-defined type and can automatically\nderive validators for case classes, tuples, sealed hierarchies and various\nstandard Scala types by composing other defined or derived validators.\n\n### Example\n\nLet's consider example business domain.\n\n```scala\ncase class UserId(id: Int) extends AnyVal\n\ncase class Email(address: String) extends AnyVal\n\ncase class PostalCode(code: String) extends AnyVal\n\ncase class Address(street: String,\n                   postalCode: PostalCode,\n                   city: String)\n\ncase class User(id: UserId,\n                email: Email,\n                address: Address)\n```\n\nLet's define validation rules as implicit type class instances.\n\n```scala\n// Usually you want to put them into companion objects\n// or group them together in a module.\n\nimport octopus.dsl._\n\nimplicit val userIdValidator: Validator[UserId] = Validator[UserId]\n  .rule(_.id \u003e 0, \"must be positive number\")\n\nimplicit val emailValidator: Validator[Email] = Validator[Email]\n  .rule(_.address.nonEmpty, \"must not be empty\")\n  .rule(_.address.contains(\"@\"), \"must contain @\")\n  .rule(_.address.split('@').last.contains(\".\"), \"must contain . after @\")\n\nimplicit val potalCodeValidator: Validator[PostalCode] = Validator[PostalCode]\n  .ruleVC((_: String).length == 5, \"must be of length 5\")\n  .ruleVC((_: String).forall(_.isDigit), \"must contain only digits\")\n\nimplicit val addressValidator: Validator[Address] = Validator\n  .derived[Address] // derives default validator for Address\n  .rule(_.city, (_: String).nonEmpty, \"must not be empty\")\n  .rule(_.street, (_: String).nonEmpty, \"must not be empty\")\n```\n\nThen, having validator instances imported, we can validate\nour bigger case classes for free, without any additional boilerplate!\n\n```scala\nimport octopus.syntax._\n\nval user1 = User(\n  UserId(1),\n  Email(\"abc@example.com\"),\n  Address(\"Love Street\", PostalCode(\"12345\"), \"Los Angeles\")\n)\n\nval user2 = User(\n  UserId(0),\n  Email(\"abc@xyz\"),\n  Address(\"\", PostalCode(\"qqqqqq\"), \"   \")\n)\n\nuser1.isValid // : Boolean = true\n\nuser1.validate.toEither // : Either[octopus.ValidationError, User] = Right(user1)\n\nuser2.isValid // : Boolean = false\n\nuser2.validate.toFieldErrMapping\n// : List[(String, String)] = List(\n//     (id,must be positive number), \n//     (email,must contain . after @),\n//     (address.postalCode,must be of length 5), \n//     (address.postalCode,must contain only digits),\n//     (address.street,must not be empty)\n// )\n```\n\n### Getting started\n\nOctopus is currently available for Scala 2.11, 2.12, 2.13 and Scala.js.\n\nTo get started with SBT, add following line to your `build.sbt`:\n\n```scala\nlibraryDependencies += \"com.github.krzemin\" %% \"octopus\" % \"0.4.1\"\n```\n\nOr if you are using Scala.js:\n\n```scala\nlibraryDependencies += \"com.github.krzemin\" %%% \"octopus\" % \"0.4.1\"\n```\n\n### Integration with Cats / Scalaz\n\nThere are available additional modules that simplify integration with\nCats and Scalaz validation types.\n\n#### Cats\n\nIf you want to integrate with Cats, simply add following line to `build.sbt`:\n\n```scala\nlibraryDependencies += \"com.github.krzemin\" %%% \"octopus-cats\" % \"0.4.1\"\n```\n\nHaving this dependency on classpath, you can use \n\n```scala\nimport octopus.syntax._\nimport octopus.cats._\n\nuser1.validate.toValidatedNel // : ValidatedNel[octopus.ValidationError, User] = Valid(user1)\n\nuser2.validate.toValidatedNel // : ValidatedNel[octopus.ValidationError, User] = Invalid(NonEmptyList(...))\n```\n\nSee [integration test suite](https://github.com/krzemin/octopus/blob/master/octopusCats/src/test/scala/octopus/cats/CatsIntegrationSpec.scala)\nfor more information.\n\n\n#### Scalaz\n\nAlternatively, if you want similar integration with Scalaz, add following line to `build.sbt`:\n\n```scala\nlibraryDependencies += \"com.github.krzemin\" %%% \"octopus-scalaz\" % \"0.4.1\"\n```\n\nSee [integration test suite](https://github.com/krzemin/octopus/blob/master/octopusScalaz/src/test/scala/octopus/scalaz/ScalazIntegrationSpec.scala)\nfor reference.\n\n\n### Asynchronous validators\n\nSometimes validation rules are more complex in sense that they can't be decided\nlocally by only looking at object value, but they require some external context\nlike querying service or database. Therefore, Octopus has support for asynchronous\npredicates, that instead of `T =\u003e Boolean`, are defined in terms of `T =\u003e Future[Boolean]`.\nThe same as with normal validation predicates, full derivation is also supported for\nasynchronous validators. Look at the example below to get better insight:\n\n```scala\ntrait EmailService {\n  def isEmailTaken(email: String): Future[Boolean]\n  def doesDomainExists(email: String): Future[Boolean]\n}\n\nclass AsyncValidators(emailService: EmailService) {\n\n  implicit val emailAsyncValidator: AsyncValidator[Email] =\n    Validator\n      .derived[Email] // (1)\n      .async.ruleVC(emailService.isEmailTaken, \"email is already taken by someone else\") // (2)\n      .async.rule(_.address, emailService.doesDomainExists, \"domain does not exists\") // (3)\n}\n\nval asyncValidators = new AsyncValidators(...)\n\nimport asyncValidators._ // (4)\n\nEmail(\"abc@xyz.qux\").isValidAsync // Success(false): Future[Boolean]\nEmail(\"abc@xyzqux\").validateAsync\n  .map(_.toFieldErrMapping)\n  // Success(List((\"\", \"must contain . after @\"), (\"\", \"domain does not exists\"))): Future[List[(String, String)]\n\n\nval user1 = User(\n  UserId(1),\n  Email(\"taken@example.com\"),\n  Address(\"Love Street\", PostalCode(\"12345\"), \"Los Angeles\")\n)\n\nuser1.validateAsync\n  .map(_.toFieldErrMapping) // Success(List(\"email\", \"email is already taken by someone else\")): Future[List[(String, String)]\n```\n\nComments:\n\n* (1) we are requesting to derive usual validator for `Email` type\n* (2) by prepending rule with `async` keyword we can define validator rule with\n  asynchronous predicate that lifts our validator to `AsyncValidator[Email]`\n* (3) we are adding next asynchronous validation rule\n* (4) we are importing instances for asynchronous validators into current scope\n  so that later we can use `.isValidAsync`/`.validateAsync` extension methods.\n\n\n#### Using other monad\n\nWhen working with asynchronous validators DSL, by default you define rules and\nobtain results in scala Future. However you're not particularly tied to it.\n\nWe provide bridge instances for internal `AppError` (which resembles applicative\nfunctor with error handling capabilities) for cats `ApplicativeError` and scalaz\n`MonadError`. Any wrapper type `M[_]` for which you already have those instances,\nwill work. Otherwise, you need to define your own instance for `AppError` and make\nsure it's in implicit scope when using rule dsl.\n\nExample integration with `cats.effect.IO`:\n\n```scala\n  import octopus.async.cats.implicits._\n  import cats.effect.IO\n\n  trait EmailService {\n    def isEmailTaken(email: String): IO[Boolean]\n    def doesDomainExists(email: String): IO[Boolean]\n  }\n\n  implicit val emailAsyncValidator: AsyncValidatorM[IO, Email] =\n    Validator\n      .derived[Email]\n      .asyncM[IO].ruleVC(emailService.isEmailTaken, \"email is already taken by someone else\") // (2)\n      .async.rule(_.address, emailService.doesDomainExists, \"domain does not exists\") // (3)\n\n  Email(\"abc@xyz.qux\").isValidAsync // IO[Boolean]\n```\n\n## FAQ\n\n#### How it's different that Cats/Scalaz validation data types?\n\nThe main difference between Octopus and Cats/Scalaz validation types is an approach\nto composability of validations.\n\nCats/Scalaz validations are kind of disjunction types that hold successfully validated\nvalue or some validation error(s). They can be composed usually with simple combinators\nor applicative builder syntax. When having lot of case classes with many fields and you\nwant to compose validators of their fields, you have to do this manually which results\nwith rather lot amount of boilerplate code.\n\nOctopus approach is a bit different. The `Validator[T]` type-class holds a function\nthat will perform validation at some point of time. There are actually two levels of\ncomposability: \n\n* validation rules for single type - can be composed using provided DSL,\n* field validators that composes and create case class validator - that is achieved\n  automatically by using type-class derivation mechanism; still you can override validation\n  rules for certain types in local contexts.\n\n## License\n\nCopyright 2016-2020 Piotr Krzemiński\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkrzemin%2Foctopus","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkrzemin%2Foctopus","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkrzemin%2Foctopus/lists"}