{"id":17248844,"url":"https://github.com/serras/arrow-validation-tree","last_synced_at":"2025-04-14T05:32:27.439Z","repository":{"id":195554771,"uuid":"693157829","full_name":"serras/arrow-validation-tree","owner":"serras","description":"Arrow Validation Tree","archived":false,"fork":false,"pushed_at":"2025-04-08T01:29:59.000Z","size":118,"stargazers_count":9,"open_issues_count":8,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-08T02:29:03.125Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/serras.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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}},"created_at":"2023-09-18T13:16:55.000Z","updated_at":"2024-01-22T23:35:37.000Z","dependencies_parsed_at":null,"dependency_job_id":"f8c64ba9-cc88-498e-b879-163827c003e9","html_url":"https://github.com/serras/arrow-validation-tree","commit_stats":null,"previous_names":["serras/arrow-validation-tree"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/serras%2Farrow-validation-tree","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/serras%2Farrow-validation-tree/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/serras%2Farrow-validation-tree/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/serras%2Farrow-validation-tree/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/serras","download_url":"https://codeload.github.com/serras/arrow-validation-tree/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248826838,"owners_count":21167761,"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":[],"created_at":"2024-10-15T06:42:15.834Z","updated_at":"2025-04-14T05:32:27.075Z","avatar_url":"https://github.com/serras.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Arrow Validation Tree\n\n[Arrow](https://arrow-kt.io/) provides powerful tools to deal with \n[validation](https://arrow-kt.io/learn/typed-errors/validation/), using its generic\n[typed errors](https://arrow-kt.io/learn/typed-errors/working-with-typed-errors/) framework.\nAlas, by default any validation errors are \"flattened\" into a single value or list, and that in turn\nmakes it difficult to figure out which fields have validation errors.\n\nTo make this more concrete, let's introduce a couple of data classes:\n\n```kotlin\ndata class Person(val name: Name, val age: Int)\ndata class Name(val first: String, val last: String)\n```\n\n## Validation\n\nTo keep things simple, we'll just check that both parts of the name are not empty, and that the age\nis positive. Using Arrow's [validation](https://arrow-kt.io/learn/typed-errors/validation/) operators,\nwe come to the following:\n\n```kotlin\nimport arrow.core.*\nimport arrow.core.raise.*\n\nfun person(firstName: String, lastName: String, age: Int): Either\u003cNonEmptyList\u003cString\u003e, Person\u003e =\n  either {\n    zipOrAccumulate(\n      { ensure(firstName.isNotEmpty()) { \"first name is empty\" } },\n      { ensure(lastName.isNotEmpty()) { \"last name is empty\" } },\n      { ensure(age \u003e 0) { \"age is negative\" } }\n    ) { _, _, _ -\u003e Person(Name(firstName, lastName), age) }\n  }\n```\n\nThe signature of this function tells us that we either get back a well-formed `Person`, or a\n`NonEmptyList\u003cString\u003e` as validation errors. Since those errors are not linked to the different\ncomponents of a `Person`, we are forced to include that information as part of the message.\nThis is not great: in a user interface where errors are shown next to each input field, you\ndon't want that duplicate information.\n\nArrow Validation Tree allows you to label (or tag) the different sub-validations. You can use\nany type as label, but `KProperty` works pretty well in that case. Here's the validation of a\n`Person`, labelled accordingly.\n\n```kotlin\nimport arrow.validation.*\n\nfun person(firstName: String, lastName: String, age: Int): Either\u003cPropertyValidationTree\u003cString\u003e, Person\u003e =\n  validationTree {\n    fields(\n      Person::name to {\n        fields(\n          Name::first to { ensure(firstName.isNotEmpty()) { \"empty\" } },\n          Name::last to { ensure(lastName.isNotEmpty()) { \"empty\" } },\n        ) { _, _ -\u003e Name(firstName, lastName) }\n      },\n      Person::age to { ensure(age \u003e 0) { \"age is negative\" } }\n    ) { name, _ -\u003e Person(name, age) }\n  }\n```\n\nThe API exposed by `validationTree` is quite small. The main difference is that we need to\nprovide the labels that ultimately appear in the validation tree; we can see that we change\n`zipOrAccumulate` to `fields`. The table below summarizes the rest of the changes.\n\n| `Raise` operation | `ValidationTreeRaise` operation | Label                  |\n|-|-|------------------------|\n| `zipOrAccumulate` | `fields` | Given explicitly       |\n| `mapOrAccumulate` | `elements` | Taken from the indices |\n\n## Inspection\n\nThe result of `validationTree` in the failure case is now a `ValidationTree`, instead of simply\na `NonEmptyList`. Such tree remembers the structure of the validation, so you can more easily\ninspect the problems:\n\n```kotlin\n// the tree only contains 'problems' if any was present\nvalidationTree[Person::name][Name::first]?.problems\n// 'problemsOrEmpty' is a utility method to always get a List back\nvalidationTree[Person::age].problemsOrEmpty()\n```\n\nWe also provide _structured inspection_, in which nested blocks correspond to problems nested\nin the tree. This is useful, among other scenarios, when designing a user interface that should\ninform about those errrors.\n\n```kotlin\n@Composable\nfun personForm(\n  first: String, last: String, age: Int, \n  errors: PropertyValidationTree\u003cString\u003e\n) {\n  errors.inspect {\n    Person::name.inspect {\n      Name::first.inspect {\n        TextField(value = first, isError = hasProblems)\n        problems.forEach { Text(text = it) }\n      }\n      Name::last.inspect { /* as above */ }\n    }\n  }\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fserras%2Farrow-validation-tree","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fserras%2Farrow-validation-tree","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fserras%2Farrow-validation-tree/lists"}