{"id":16120485,"url":"https://github.com/sksamuel/tribune","last_synced_at":"2025-04-13T00:46:28.035Z","repository":{"id":8955105,"uuid":"10692938","full_name":"sksamuel/tribune","owner":"sksamuel","description":"Kotlin 'parse not validate'","archived":false,"fork":false,"pushed_at":"2025-02-10T17:09:13.000Z","size":616,"stargazers_count":203,"open_issues_count":1,"forks_count":23,"subscribers_count":10,"default_branch":"master","last_synced_at":"2025-04-13T00:46:23.425Z","etag":null,"topics":["arrow-kt","functional-programming","kotlin","ktor","validation"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","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/sksamuel.png","metadata":{"files":{"readme":"README.md","changelog":"changelog.md","contributing":null,"funding":null,"license":"LICENSE.txt","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":"2013-06-14T16:15:51.000Z","updated_at":"2025-03-30T16:16:52.000Z","dependencies_parsed_at":"2024-01-12T10:24:34.980Z","dependency_job_id":"2d70bb73-f7fe-4b0c-9016-7e373b44c8d8","html_url":"https://github.com/sksamuel/tribune","commit_stats":{"total_commits":281,"total_committers":10,"mean_commits":28.1,"dds":0.09964412811387902,"last_synced_commit":"2d7ef6efd6b60d8b5d84ef46c400593c6b955e3f"},"previous_names":["sksamuel/optio","sksamuel/monkeytail","sksamuel/princeps","sksamuel/elasticsearch-river-redis"],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sksamuel%2Ftribune","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sksamuel%2Ftribune/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sksamuel%2Ftribune/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sksamuel%2Ftribune/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sksamuel","download_url":"https://codeload.github.com/sksamuel/tribune/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248650432,"owners_count":21139672,"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":["arrow-kt","functional-programming","kotlin","ktor","validation"],"created_at":"2024-10-09T20:58:31.102Z","updated_at":"2025-04-13T00:46:28.018Z","avatar_url":"https://github.com/sksamuel.png","language":"Kotlin","funding_links":[],"categories":["Ktor Projects"],"sub_categories":["Socials"],"readme":"Tribune\n=========================\n\n_parse don't validate_ for Kotlin.\n\nInspired by [this blog post by Alexis King](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/),\nTribune is a Kotlin library for the JVM that builds on [Arrow](https://arrow-kt.io/) to provide a toolset for creating simple parsers from raw _input_ types, to properly\nvalidated _parsed_ types.\n\n[![master](https://github.com/sksamuel/tribune/actions/workflows/master.yml/badge.svg)](https://github.com/sksamuel/tribune/actions/workflows/master.yml)\n[\u003cimg src=\"https://img.shields.io/maven-central/v/com.sksamuel.tribune/tribune-core.svg?label=latest%20release\"/\u003e](http://search.maven.org/#search%7Cga%7C1%7Ctribune)\n[\u003cimg src=\"https://img.shields.io/nexus/s/https/s01.oss.sonatype.org/com.sksamuel.tribune/tribune-core.svg?label=latest%20snapshot\u0026style=plastic\"/\u003e](https://oss.sonatype.org/content/repositories/snapshots/com/sksamuel/tribune/tribune-core/)\n\nSee [Changelog](changelog.md)\n\n### Rationale\n\nNormally, when we have a system that accepts input, we validate or sanitize that input. That is, we run some checks on\nthe inputs,\nreturn some kind of error if they don't meet our requirements. For example, we want emails to contain an '@' and a zip\ncode to be digits.\nThen we continue with the request safe in the knowledge that we've done our due diligence.\n\nHere is an extremely simplified example.\n\n```kotlin\nfun validate(email: String) = name.contains(\"@\")\n\nfun persist(email: String) {\n   // write to db\n}\n\nfun handleRequest(email: String) {\n   if (!validate(email)) error(\"Not a real email\")\n   persist(email)\n}\n```\n\nBut the ultimate actioner of the input (`persist` in the above example) has to take it on faith that the input was\nvalidated, or it has to perform the validation again itself. Obviously in the 6 line example above, it is easy to see\nthat validation is taking place but as a code base grows in complexity, and the validation code drifts from the use\nsite,\nit becomes less obvious what validation is taking place and where.\n\nIn my experience, as codebases grow, our 'service' methods performing the 'logic' end up being called from\nmore and more places. Perhaps new endpoints are added which ultimately call into the same service, or feature flags\nare added which result in multiple paths sharing functions. As new developers onboard, they can distrust the existing\ncode or be unware of something added previously. How can we be sure that the validation is taking place at the right\nplaces, for all the appropriate code paths?\n\nSometimes we do the validation again, \"just to be sure\". We don't trust that the callers of our code are giving us\nproperly validated types, so we check again, just in case. Never can be too safe amirite? In these situations developers\nhave moved the validation into the 'logic' method itself, now resulting in methods doing validation as well as\nprocessing.\n\nAs input validation often results in errors being returned to the caller, the deeper in the stack we perform these\noperations, the more boilerplate we need to bubble them back out. We can throw an exception and let it propagate out,\nor thread a `Result` instance back to the caller, but in both cases our entry point has to disambiguate parse errors\nfrom other errors, muddying the error handling.\n\nWe can rely on tests. That's how it's done in dynamic languages where you don't know the data type you're getting, so\nyou have to use faith and a solid test suite. But we're using a compiled language, and isn't a compiled language\nsupposed to leverage the compiler to create more robust code?\n\nWhen we validate something, we are adding information. If we validate that a string is a valid email, we have added\nthe \"is an email\" assertion to the original string. When we validate and then continue with the original types,\nwe are not passing that extra information to the caller. Why aren't we using the rich type system of a compiled language\nto help us catch validation errors?\n\nIf we indicate through types that our input had already been validated, then we could trust that\ninput. One way to do this is to have a type that represents the \"checked and validated\" result of the original input.\n\nThis is what we mean when we say _parsing not validating_.\n\n### Parsers\n\nIn Tribune, a parser is a function\nfrom an input type to a valid or invalid result. A valid result contains the _parsed_ type and should be a wrapper type\nthat indicates the extra validation that has been performed.\nAn invalid result\ncontains one or more errors in the form of a `NonEmptyList`. The actual type returned is an [Arrow](https://arrow-kt.io/) `EitherNel\u003cE, O\u003e.\n\nA parser has three type parameters, the first being the input type, the second being the parsed type (or output type), and the third\nbeing the error type. The error type can be your own ADT or just plain strings. In this example we will use strings.\nThe ADT approach is powerful when you want fine control over error handling, but if we are more interested in the\nrobustness factor than how errors are reported, strings will suffice.\n\nWe create a `Parser` from our input type - in this case a nullable `String`. Our initial parser is always\na pass-through parser that just returns the input as-is and which allows us to add further constraints.\nNote that the error type is `Nothing` because on the pass through parser, there are no errors to report.\n\n```kotlin\nval parser: Parser\u003cString?, String?, Nothing\u003e = Parsers.nullableString\n```\n\nNext we can add more constraints with appropriate error messages. Each additional\nconstraint we add narrows the output type. For example, if we constrain a nullable string to disallow nulls, then the\nparser's output type will narrow to be a non-nullable String. The input type does not change, because that is\nrepresenting our initial raw input. Note that the error type will change to the type of the error you provide, in\nthis case a string also.\n\n```kotlin\nval parser: Parser\u003cString?, String, String\u003e =\n   Parsers.nullableString\n      .notNullOrBlank { \"Must be provided\" }\n```\n\nWe could further constrain this input to be an int:\n\n```kotlin\nval parser: Parser\u003cString?, Int, String\u003e =\n   Parsers.nullableString\n      .notNullOrBlank { \"Must be provided\" }\n      .int { \"must be int\" }\n```\n\nThere are many methods available on a parser, eg `filter`, `minlen`, `enum`, `contramap` and so on.\nExplore in your IDE to see the full set.\n\n#### Wrapping\n\nOnce we're finished with validation, we want to then wrap in a parsed type. We can do this with `map`:\n\n```kotlin\nval parser: Parser\u003cString?, MyParsedType, String\u003e =\n   Parsers.nullableString\n      .notNullOrBlank { \"Must be provided\" }\n      .int { \"must be int\" }\n      .map { MyParsedType(it) }\n```\n\nParsers are invoked using the `parse` method. Eg:\n\n```kotlin\nparser.parse(\"abc\") // must be int\nparser.parser(\"123\") // success!\n```\n\nOr if you want a null instead of errors, you can use `parseOrNull`. Eg:\n\n```kotlin\nparser.parseOrNull(\"abc\") // must be an int, so null is returned\nparser.parser(\"123\") // success!\n```\n\n#### Full Example\n\nWe start by creating a type to represent a validated and sanitized value. Let's say we want to validate that input\nstrings are valid ISBN codes. They must be 10 or 13 digit codes, and 13 digit codes must start with a 9.\nThe parsed type will be called `Isbn`.\n\n```kotlin\ndata class Isbn(val value: String) {\n   init {\n      require(value.length == 10 || value.length == 13)\n      require(value.length == 10 || value.startsWith(\"9\"))\n   }\n}\n```\n\nNext our parser will include the validation logic, ultimately wrapping in the `Isbn` type:\n\n```kotlin\nval isbnParser =\n   Parsers.nullableString\n      .notNullOrBlank { \"ISBN must be provided\" }\n      .map { it.replace(\"-\", \"\") } // remove dashes\n      .length({ it == 10 || it == 13 }) { \"Valid ISBNs have length 10 or 13\" }\n      .filter({ it.length == 10 || it.startsWith(\"9\") }, { \"13 Digit ISBNs must start with 9\" })\n      .map { Isbn(it) }\n```\n\nThen we can parse ISBN codes:\n\n```kotlin\nisbnParser.parse(\"9783161484100\") // good!\nisbnParser.parse(\"978-3-16-148410-0\") // good!\nisbnParser.parse(\"ABC-3-16-148410-0\") // bad!\nisbnParser.parse(\"978-3-16-148410\") // bad!\n```\n\n\n#### Composing\n\nWe've seen how we can have a parser for a simple type, but most of the time we have complex types, and we want\nto validate each field. We achieve this in Tribune using `Parser.compose`. This function accepts one or more parsers,\nand then a _mapping_ function which combines all the valid results into a single type. If any component parser fails,\nall\nthe errors will be combined and returned.\n\nNote that this _mapping_ function can be the constructor of a data class for ease of use.\n\nWe must also provide an input type which has all the individual inputs wrapped together. In HTTP services, this\ninput type is often your deserialized type from the request.\n\nFor example, we will create parsers for cities, zips and countries and then combine them into a single address parser.\n\nWe start by creating a type to contain the non-validated inputs, and then the validated output type.\n\n```kotlin\ndata class AddressInput(\n   val city: String?,\n   val zip: String?,\n   val country: String?,\n)\n\ndata class Address(\n   val city: City,\n   val zip: Zipcode,\n   val country: CountryCode,\n)\n\ndata class City(val value: String)\ndata class Zipcode(val value: String)\ndata class CountryCode(val value: String)\n```\n\nNext we create a parser for each field:\n\n```kotlin\nval cityParser = Parsers\n   .nonBlankString { \"City must be provided\" }\n   .map { City(it) }\n\nval zipcodeParser = Parsers\n   .nonBlankString { \"Zipcode must be provided\" }\n   .length(5) { \"Zipcode should be 5 digits\" }\n   .map { Zipcode(it) }\n\nval countryCodeParser = Parsers\n   .nonBlankString { \"CountryCode must be provided\" }\n   .length(2) { \"CountryCode should be 2 digits\" }\n   .map { CountryCode(it) }\n```\n\nFinally, we combine these together. Note the use of contramap here, this is how we _extract_ the appropriate field\nfrom the input object to pass to each component parser.\n\n```kotlin\nval addressParser = Parser.compose(\n   cityParser.contramap { it.city },\n   zipcodeParser.contramap { it.zip },\n   countryCodeParser.contramap { it.country },\n   ::Address\n)\n```\n\nNow we can use this parser like:\n\n```kotlin\naddressParser.parse(AddressInput(\"Chicago\", \"60011\", \"US\")) // valid!\naddressParser.parse(AddressInput(\"Chicago\", \"60ABC\", \"US\")) // invalid!\naddressParser.parse(AddressInput(\"Chicago\", \"60011\", \"Krypton\")) // invalid!\naddressParser.parse(AddressInput(null, \"60011\", \"Krypton\")) // invalid!\naddressParser.parse(AddressInput(null, null, null)) // invalid!\n```\n\n### Ktor Integration\n\nTribune provides [Ktor](https://ktor.io) integration through the optional `tribune-ktor` module.\n\nOnce this is added to your build, you can use `withParsedBody` inside your Ktor routes. This function requires\na parser and an optional _error handler_. The request body is retrieved as an instance of the parser input type,\nand then passed to the parser.\n\nIf the parser returns errors, the _error handler_ is invoked to return an error response to the caller. Tribune provides\nseveral error handlers out of the box. A full list of provided error handlers is listed later in this document.\n\nHere is an example of `withParsedBody`, reusing the earlier parser for ISBN book codes.\n\nThis parser is used inside a POST endpoint and if valid, we respond with a 201, otherwise the default\nhandler is used (returns 400 Bad Request with a JSON body of errors).\n\n```kotlin\nrouting {\n   post(\"/isbn\") {\n      withParsedBody(isbnParser) { isbn -\u003e\n         println(\"Parsed ISBN $isbn\")\n         call.respond(HttpStatusCode.Created)\n      }\n   }\n}\n```\n\nThis table lists the handlers provided out of the box:\n\n| Handler             | Description                                                                                                                                                              |\n|---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `jsonHandler`       | Returns a 400 Bad Request with a JSON array, where each error is an element. Each error is converted to a String through .toString() before being included in the array. |\n| `textPlainHandler`  | Returns a 400 Bad Request with a text/plain body, which is the list of errors concatented into a simple string                                                           |\n| `loggingHandler`    | Writes the errors to info level logging, and does not return a response or body. This should be composed with another handler.                                           |\n| `badRequestHandler` | Returns an error response as a 400 Bad Request without a body. This is suitable for when we don't want to return error details to the caller.                            |\n\nHandlers can be composed together using the `compose` extension function on a handler.\nEg, to use the logging handler with the json handler, we can do:\n\n```kotlin\nwithParsedBody(parser, loggingHandler.compose(jsonHandler)) { parsed -\u003e\n   println(\"Parsed input $parsed\")\n   call.respond(HttpStatusCode.OK)\n}\n```\n\n### Using tribune in your project\n\n```groovy\ncompile 'com.sksamuel.tribune:tribune-core:x.x.x'\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsksamuel%2Ftribune","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsksamuel%2Ftribune","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsksamuel%2Ftribune/lists"}