{"id":18026550,"url":"https://github.com/hadilq/happy","last_synced_at":"2025-03-27T01:31:15.446Z","repository":{"id":40461232,"uuid":"326256632","full_name":"hadilq/happy","owner":"hadilq","description":"This library provides an annotation to auto-generate a Kotlin DSL for `sealed` classes to make working with them more concise. You can find more in https://hadilq.com/posts/happy-railway/","archived":false,"fork":false,"pushed_at":"2022-05-18T15:35:40.000Z","size":190,"stargazers_count":9,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-22T21:06:26.204Z","etag":null,"topics":["annotation-processing","dsl","kapt","kotlin","ksp","sealed-class"],"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/hadilq.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}},"created_at":"2021-01-02T19:32:22.000Z","updated_at":"2024-11-05T15:05:45.000Z","dependencies_parsed_at":"2022-08-09T21:12:01.035Z","dependency_job_id":null,"html_url":"https://github.com/hadilq/happy","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hadilq%2Fhappy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hadilq%2Fhappy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hadilq%2Fhappy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hadilq%2Fhappy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hadilq","download_url":"https://codeload.github.com/hadilq/happy/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245764657,"owners_count":20668456,"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":["annotation-processing","dsl","kapt","kotlin","ksp","sealed-class"],"created_at":"2024-10-30T08:07:18.632Z","updated_at":"2025-03-27T01:31:14.316Z","avatar_url":"https://github.com/hadilq.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"![Health Check](https://github.com/hadilq/happy/workflows/Health%20Check/badge.svg?branch=main)\n[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.hadilq/happy-processor/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.hadilq/happy-processor)\n\n# Happy\n\nThis library provides an annotation to auto-generate a Kotlin DSL for `sealed` classes to make\nworking with them more concise.\nThe standard way to handle different cases of a `sealed` class in Kotlin is using `when` statement/expression.\nHowever, there looks like a more kotliny way to do that, where this annotation processor tries to address.\n\nIt named **Happy** to refer to the notion that each `sealed` class as a result have one and only one **happy** path.\nAlso, you may know Kotlin is `fun`, why not make it `Happy` too?\n\n\n## How Concise\n\nYou may know Kotlin null-safety is what developers love to work with. Did you ask yourself why?\nLet's have a closer look. In Kotlin, we have safe call operator, `?.`, and elvis operator, `?:`,\nwhere make working with nulls so easier. The general idea is to implement the same type as\n`java.util.Optional` but with shorter names. By the way, it's not the end of story!\nLet's look how we use them.\n```kotlin\nval l = b?.length ?: -1\n```\nHere the `b?.length` is a process that have two states, either `b` is available and have a `length`\nor the result is not available, where it returns `null` and we replace it with `-1`.\nTwo states? Yes! As mentioned, the null-safety is like `Optional` where has two states.\nBasically, it's a sum type.\nSum types or [disjoints](https://en.wikipedia.org/wiki/Coproduct) are types that when you want to think\nof them as a result, you use \"or\" in your sentence, for instance, `b?.length` returns a `value`, as the happy path, **or** `null`.\nGenerally in Kotlin we use `sealed` classes as sum-types. So the magic of Kotlin in null-safety happens\nwhere we deal with a happy path differently from failed ones. This is the moment of AHA!\nThen why `when` in Kotlin doesn't respect to happy paths!\nI don't know, but this library wants to fill this gap by introducing this happy DSL to fill the gap.\n\n## Usage\nThe usage is similar to [Elvis operator](https://kotlinlang.org/docs/reference/null-safety.html#elvis-operator)\nespecially for two cases `sealed` classes.\n\n### Two Case\nFor instance, if you have a `sealed` class like\n```kotlin\nsealed class A {\n  @Happy\n  object HappyA : A()\n  object FailedA : A()\n}\n```\nwhere it's clear that `HappyA` is the **happy** path if a process uses `A` as the result and `FailedA` is the\nfailure of the process. Notice we tagged the happy path with `@Happy` annotation.\nLet the `doWork` has a result of `A` then the happy DSL would looks like\n```kotlin\nfun doWork(): A = ....\n\nfun doJob(): B {\n  val result: HappyA = doWork() elseIf {\n    // Handle the failure.\n  }\n    ...\n}\n```\nTo handle the failure you have three options:\n - You can have another method to fix the failure and replace the `FailedA` with `HappyA`,\n   for instance bring back the default, or in case of UnAuthorized\n   exception can request to authorize. It would be like\n```kotlin\nfun doJob(): B {\n  val result: HappyA = doWork() elseIf ::handleFailure\n    ...\n}\n```\n- You can break the process and return the failure of `B` type.\n```kotlin\nfun doJob(): B {\n  val result: HappyA = doWork() elseIf {\n    return B.failure()\n  }\n    ...\n}\n```\n- Of course, you can break it with throwing an exception, which is a dirty approach IMHO.\nI just mentioned it to have a complete view.\n```kotlin\nfun doJob(): B {\n  val result: HappyA = doWork() elseIf {\n    throw ...\n  }\n    ...\n}\n```\n\n### Cases' Properties\nWhat will happen if `FailedA` has properties? Not so much difference, but the\nlambda function will pass the properties. For instance, assume the following.\n```kotlin\nsealed class A {\n  @Happy\n  object HappyA : A()\n  class FailedA(val why: Int) : A()\n}\n```\nso the lambda function will be like\n```kotlin\nfun doJob(): B {\n  val result: HappyA = doWork() elseIf { why -\u003e // The property of `FailedA`\n    return B.failure(why)\n  }\n  ...\n}\n```\n\n\n### More Than Two Case\nFor instance, if you have a `sealed` class like\n```kotlin\nsealed class A {\n  @Happy\n  object HappyA : A()\n  object OptionOne : A()\n  class OptionTwo(val why: Int) : A()\n}\n```\nwhere it's clear that `HappyA` is the `Happy` path if a process uses `A` as the result.\n`OptionOne` and `OptionTwo` are the failures.\nSo the usage will be like\n```kotlin\nfun doJob(): B {\n  val result: HappyA = doWork() elseIf {\n      OptionOne(::handleOptionOne)\n      OptionTwo { why -\u003e // The property of `OptionTwo`\n        return B.failure(why)\n      }\n  }\n  ...\n}\n```\nWe assumed that `handleOptionOne` will be able to fix the `OptionOne` failure, but\n`OptionTwo` is not fixable, so we returned the failure of `doJob` method.\n\n### Nested Cases\nFor instance, if you have a sealed class like\n```kotlin\nsealed class A {\n   @Happy\n   class HappyA : A()\n   abstract class SituationOne : A() {\n      object OptionOne : SituationOne()\n      class OptionTwo(val why: Int, val where: Int) : SituationOne()\n   }\n\n   abstract class SituationTwo : A() {\n      class OptionThree(val where: Int) : SituationTwo()\n      class OptionFour(val how: Int) : SituationTwo()\n   }\n}\n```\nthe happy DSL will be like\n```kotlin\nfun doJob(): B {\n   val result: HappyA = doWork() elseIf {\n      SituationOneOptionOne(::handleOptionOne)\n      SituationOneOptionTwo { why, where -\u003e // The properties of `OptionTwo`\n         return B.failure(why)\n      }\n      SituationTwoOptionThree(::handleOptionThree)\n      SituationTwoOptionFour(::handleOptionFour)\n   }\n   ...\n}\n```\nDid you notice the naming? That's the difference.\n\n# Elvis\nSince `0.0.3`, the Happy processor generates `elvis` function too, to have a\nmore typesafe experience. The only disadvantage of `elvis` function is that it\nisn't an `infix` function for more than two cases sealed classes, so a user who\nwants to practice Happy Railway may not be satisfied. Check out [RailwayTest.kt](https://github.com/hadilq/happy/blob/main/happy-sample/src/test/kotlin/com/github/hadilq/happy/sample/RailwayTest.kt)\nand [RailwayElvisTest.kt](https://github.com/hadilq/happy/blob/main/happy-sample/src/test/kotlin/com/github/hadilq/happy/sample/RailwayElvisTest.kt)\nfor more comparison. Anyway! For this `sealed class`\n```kotlin\nsealed class A {\n  @Happy\n  object HappyA : A()\n  object OptionOne : A()\n  class OptionTwo(val why: Int) : A()\n}\n```\nit looks like this\n```kotlin\nfun doJob(): B {\n  val result: HappyA = doWork().elvis(\n      OptionOne = ::handleOptionOne,\n      OptionTwo = { failure: OptionTwo -\u003e\n        return B.failure(failure.message)\n      },\n  )\n  ...\n}\n```\nAlso, for two cases like\n```kotlin\nsealed class A {\n   @Happy\n   object HappyA : A()\n   object OptionOne : A()\n}\n```\nit's an `infix` function so it's so similar to `elseIf` counterpart.\n```kotlin\nfun doJob(): B {\n  val result: HappyA = doWork() elvis { failure: OptionOne -\u003e\n    return B.failure(failure.message)\n  }\n  ...\n}\n```\n\nIf you're not satisfied with above explanations and the tests to how it's beneficial for your code, you can also take\na look at the `happy-processor-common` code, where it's used in its processor too!\n\n## Download\n\nDownload via gradle for `kapt`\n\n```groovy\nimplementation \"com.github.hadilq:happy-annotation:$libVersion\"\nkapt \"com.github.hadilq:happy-processor:$libVersion\"\n```\n\nor download for `ksp`\n\n```groovy\nimplementation \"com.github.hadilq:happy-annotation:$libVersion\"\nksp \"com.github.hadilq:happy-processor-ks:$libVersion\"\n```\n\nwhere you can find the `libVersion` in the [Releases](https://github.com/hadilq/happy/releases) page of this repository.\n\nIf you are using `ksp` don't forget to follow their [documents](https://kotlinlang.org/docs/ksp-quickstart.html), especially the [IDE related part](https://kotlinlang.org/docs/ksp-quickstart.html#make-ide-aware-of-generated-code).\n\nSnapshots of the development version are available in [Sonatype's snapshots repository](https://oss.sonatype.org/content/repositories/snapshots).\n\n## Contribution\n\nJust create your branch from the main branch, change it, write additional tests, satisfy all tests,\ncreate your pull request, thank you, you're awesome.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhadilq%2Fhappy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhadilq%2Fhappy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhadilq%2Fhappy/lists"}