{"id":13611630,"url":"https://github.com/slackhq/EitherNet","last_synced_at":"2025-04-13T05:33:05.747Z","repository":{"id":38040660,"uuid":"297518695","full_name":"slackhq/EitherNet","owner":"slackhq","description":"A multiplatform, pluggable, and sealed API result type for modeling network API responses.","archived":false,"fork":false,"pushed_at":"2024-12-10T17:53:46.000Z","size":528,"stargazers_count":782,"open_issues_count":3,"forks_count":27,"subscribers_count":14,"default_branch":"main","last_synced_at":"2025-04-06T19:08:21.484Z","etag":null,"topics":["java","kotlin","multiplatform","okhttp","retrofit"],"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/slackhq.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE.txt","code_of_conduct":".github/CODE_OF_CONDUCT.md","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":"2020-09-22T02:54:29.000Z","updated_at":"2025-04-03T14:37:54.000Z","dependencies_parsed_at":"2023-01-31T19:46:16.298Z","dependency_job_id":"463390eb-388b-4879-b43e-96f26af85e2c","html_url":"https://github.com/slackhq/EitherNet","commit_stats":null,"previous_names":[],"tags_count":18,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/slackhq%2FEitherNet","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/slackhq%2FEitherNet/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/slackhq%2FEitherNet/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/slackhq%2FEitherNet/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/slackhq","download_url":"https://codeload.github.com/slackhq/EitherNet/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248670513,"owners_count":21142896,"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":["java","kotlin","multiplatform","okhttp","retrofit"],"created_at":"2024-08-01T19:01:59.268Z","updated_at":"2025-04-13T05:33:05.734Z","avatar_url":"https://github.com/slackhq.png","language":"Kotlin","readme":"# EitherNet\n\nA multiplatform, pluggable, and sealed API result type for modeling network API responses. Currently, this is\nonly implemented for [Retrofit](https://github.com/square/retrofit) on the JVM, but the core API is defined in\ncommon code and can be implemented for other platforms/frameworks.\n\nThe rest of the README below focuses on the Retrofit implementation.\n\n## Usage\n\nBy default, Retrofit uses exceptions to propagate any errors. This library leverages Kotlin sealed types\nto better model these responses with a type-safe single point of return and no exception handling needed!\n\nThe core type for this is `ApiResult\u003cout T, out E\u003e`, where `T` is the success type and `E` is a possible\nerror type.\n\n`ApiResult` has two sealed subtypes: `Success` and `Failure`. `Success` is typed to `T` with no\nerror type and `Failure` is typed to `E` with no success type. `Failure` in turn is represented by\nfour sealed subtypes of its own: `Failure.NetworkFailure`, `Failure.ApiFailure`, `Failure.HttpFailure`,\nand `Failure.UnknownFailure`. This allows for simple handling of results through a consistent,\nnon-exceptional flow via sealed `when` branches.\n\n```kotlin\nwhen (val result = myApi.someEndpoint()) {\n  is Success -\u003e doSomethingWith(result.response)\n  is Failure -\u003e when (result) {\n    is NetworkFailure -\u003e showError(result.error)\n    is HttpFailure -\u003e showError(result.code)\n    is ApiFailure -\u003e showError(result.error)\n    is UnknownFailure -\u003e showError(result.error)\n  }\n}\n```\n\nUsually, user code for this could just simply show a generic error message for a `Failure`\ncase, but the sealed subtypes also allow for more specific error messaging or pluggability of error\ntypes.\n\nSimply change your endpoint return type to the typed `ApiResult` and include our call adapter and\ndelegating converter factory.\n\n\n```kotlin\ninterface TestApi {\n  @GET(\"/\")\n  suspend fun getData(): ApiResult\u003cSuccessResponse, ErrorResponse\u003e\n}\n\nval api = Retrofit.Builder()\n  .addConverterFactory(ApiResultConverterFactory)\n  .addCallAdapterFactory(ApiResultCallAdapterFactory)\n  .build()\n  .create\u003cTestApi\u003e()\n```\n\nIf you don't have custom error return types, simply use `Unit` for the error type.\n\n### Decoding Error Bodies\n\nIf you want to decode error types in `HttpFailure`s, annotate your endpoint with `@DecodeErrorBody`:\n\n```kotlin\ninterface TestApi {\n  @DecodeErrorBody\n  @GET(\"/\")\n  suspend fun getData(): ApiResult\u003cSuccessResponse, ErrorResponse\u003e\n}\n```\n\nNow a 4xx or 5xx response will try to decode its error body (if any) as `ErrorResponse`. If you want to\ncontextually decode the error body based on the status code, you can retrieve a `@StatusCode` annotation\nfrom annotations in a custom Retrofit `Converter`.\n\n```kotlin\n// In your own converter factory.\noverride fun responseBodyConverter(\n  type: Type,\n  annotations: Array\u003cout Annotation\u003e,\n  retrofit: Retrofit\n): Converter\u003cResponseBody, *\u003e? {\n  val (statusCode, nextAnnotations) = annotations.statusCode()\n    ?: return null\n  val errorType = when (statusCode.value) {\n    401 -\u003e Unauthorized::class.java\n    404 -\u003e NotFound::class.java\n    // ...\n  }\n  val errorDelegate = retrofit.nextResponseBodyConverter\u003cAny\u003e(this, errorType.toType(), nextAnnotations)\n  return MyCustomBodyConverter(errorDelegate)\n}\n```\n\nNote that error bodies with a content length of 0 will be skipped.\n\n### Plugability\n\nA common pattern for some APIs is to return a polymorphic `200` response where the data needs to be\ndynamically parsed. Consider this example:\n\n```JSON\n{\n  \"ok\": true,\n  \"data\": {\n    ...\n  }\n}\n```\n\nThe same API may return this structure in an error event\n\n```JSON\n{\n  \"ok\": false,\n  \"error_message\": \"Please try again.\"\n}\n```\n\nThis is hard to model with a single concrete type, but easy to handle with `ApiResult`. Simply\nthrow an `ApiException` with the decoded error type in a custom Retrofit `Converter` and it will be\nautomatically surfaced as a `Failure.ApiFailure` type with that error instance.\n\n```kotlin\n@GET(\"/\")\nsuspend fun getData(): ApiResult\u003cSuccessResponse, ErrorResponse\u003e\n\n// In your own converter factory.\nclass ErrorConverterFactory : Converter.Factory() {\n  override fun responseBodyConverter(\n    type: Type,\n    annotations: Array\u003cout Annotation\u003e,\n    retrofit: Retrofit\n  ): Converter\u003cResponseBody, *\u003e? {\n    // This returns a `@ResultType` instance that can be used to get the error type via toType()\n    val (errorType, nextAnnotations) = annotations.errorType() ?: return null\n    return ResponseBodyConverter(errorType.toType())\n  }\n\n  class ResponseBodyConverter(\n    private val errorType: Type\n  ) : Converter\u003cResponseBody, *\u003e {\n    override fun convert(value: ResponseBody): String {\n      if (value.isErrorType()) {\n        val errorResponse = ...\n        throw ApiException(errorResponse)\n      } else {\n        return SuccessResponse(...)\n      }\n    }\n  }\n}\n```\n\n### Retries\n\nA common pattern in making network requests is to retry with exponential backoff. EitherNet ships with a highly configurable `retryWithExponentialBackoff()` function for this case.\n\n```kotlin\n// Defaults for reference\nval result = retryWithExponentialBackoff(\n  maxAttempts = 3,\n  initialDelay = 500.milliseconds,\n  delayFactor = 2.0,\n  maxDelay = 10.seconds,\n  jitterFactor = 0.25,\n  onFailure = null, // Optional Failure callback for logging\n) {\n    api.getData()\n}\n```\n\n## Testing\n\nEitherNet ships with a [Test Fixtures](https://docs.gradle.org/current/userguide/java_testing.html#sec:java_test_fixtures)\nartifact containing a `EitherNetController` API to allow for easy testing with EitherNet APIs. This\nis similar to OkHttp’s `MockWebServer`, where results can be enqueued for specific endpoints.\n\nSimply create a new controller instance in your test using one of the `newEitherNetController()` functions.\n\n```kotlin\nval controller = newEitherNetController\u003cPandaApi\u003e() // reified type\n```\n\nThen you can access the underlying faked `api` property from it and pass that on to whatever’s being tested.\n\n\n```kotlin\n// Take the api instance from the controller and pass it to whatever's being tested\nval provider = PandaDataProvider(controller.api)\n```\n\nFinally, enqueue results for endpoints as needed.\n\n```kotlin\n// Later in a test you can enqueue results for specific endpoints\ncontroller.enqueue(PandaApi::getPandas, ApiResult.success(\"Po\"))\n```\n\nYou can also optionally pass in full suspend functions if you need dynamic behavior\n\n```kotlin\ncontroller.enqueue(PandaApi::getPandas) {\n  // This is a suspend function!\n  delay(1000)\n  ApiResult.success(\"Po\")\n}\n```\n\nIn instrumentation tests with DI, you can provide the controller and its underlying API in a test\nmodule and replace the standard one. This works particularly well with [Anvil](https://github.com/square/anvil).\n\n```kotlin\n@ContributesTo(\n  scope = UserScope::class,\n  replaces = [PandaApiModule::class] // Replace the standard module\n)\n@Module\nobject TestPandaApiModule {\n  @Provides\n  fun providePandaApiController(): EitherNetController\u003cPandaApi\u003e = newEitherNetController()\n\n  @Provides\n  fun providePandaApi(\n    controller: EitherNetController\u003cPandaApi\u003e\n  ): PandaApi = controller.api\n}\n```\n\nThen you can inject the controller in your test while users of `PandaApi` will get your test instance.\n\n### Java Interop\n\nFor Java interop, there is a limited API available at `JavaEitherNetControllers.enqueueFromJava`.\n\n### Validation\n\n`EitherNetController` will run some small validation on API endpoints under the hood. If you want to\nadd your own validations on top of this, you can provide implementations of `ApiValidator` via\n`ServiceLoader`. See `ApiValidator`'s docs for more information.\n\n## Installation\n\n[![Maven Central](https://img.shields.io/maven-central/v/com.slack.eithernet/eithernet.svg)](https://mvnrepository.com/artifact/com.slack.eithernet/eithernet)\n```gradle\ndependencies {\n  implementation(\"com.slack.eithernet:eithernet:\u003cversion\u003e\")\n  implementation(\"com.slack.eithernet:eithernet-integration-retrofit:\u003cversion\u003e\")\n\n  // Test fixtures\n  testImplementation(testFixtures(\"com.slack.eithernet:eithernet:\u003cversion\u003e\"))\n}\n```\n\nSnapshots of the development version are available in [Sonatype's `snapshots` repository][snap].\n\nLicense\n--------\n\n    Copyright 2020 Slack Technologies, LLC\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n\n\n[snap]: https://oss.sonatype.org/content/repositories/snapshots/com/slack/eithernet/\n","funding_links":[],"categories":["REST错误处理","Kotlin"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fslackhq%2FEitherNet","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fslackhq%2FEitherNet","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fslackhq%2FEitherNet/lists"}