{"id":15014370,"url":"https://github.com/fntz/omhs","last_synced_at":"2025-04-12T07:45:10.376Z","repository":{"id":47402546,"uuid":"340640231","full_name":"fntz/omhs","owner":"fntz","description":"One More Http Server: Routing DSL on Netty.","archived":false,"fork":false,"pushed_at":"2022-10-19T11:03:04.000Z","size":296,"stargazers_count":8,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-12T07:45:01.693Z","etag":null,"topics":["http-server","netty","netty4","scala"],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fntz.png","metadata":{"files":{"readme":"Readme.md","changelog":null,"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-02-20T11:40:21.000Z","updated_at":"2022-10-19T11:03:08.000Z","dependencies_parsed_at":"2022-08-20T02:01:44.167Z","dependency_job_id":null,"html_url":"https://github.com/fntz/omhs","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fntz%2Fomhs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fntz%2Fomhs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fntz%2Fomhs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fntz%2Fomhs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fntz","download_url":"https://codeload.github.com/fntz/omhs/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248536986,"owners_count":21120687,"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":["http-server","netty","netty4","scala"],"created_at":"2024-09-24T19:45:31.776Z","updated_at":"2025-04-12T07:45:10.357Z","avatar_url":"https://github.com/fntz.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n# One More Http Server\n\nRouting DSL on Netty\n \n* no additional dependencies (shapeless/scalaz/zio/cats/etc...), only Netty (scala-reflect for compiling time)\n\n# install:\n\n```scala\n\"com.github.fntz\" %% \"omhs-dsl\" % \"0.0.5\"\n// play-json support\n\"com.github.fntz\" %% \"omhs-play-support\" % \"0.0.5\"\n// circle-json support\n\"com.github.fntz\" %% \"omhs-circe-support\" % \"0.0.5\"\n// jsoniter support\n\"com.github.fntz\" %% \"omhs-jsoniter-support\" % \"0.0.5\"\n```\n\n### before work:\n1. add `-Ydelambdafy:inline` in `scalacOptions`\n2. add reflect: `\"org.scala-lang\" % \"scala-reflect\" % scalaVersion.value % \"compile\"`\n3. add `Netty` libraries: \n\n```sbt\n\"io.netty\" % \"netty-codec-http\" % NettyVersion,\n\"io.netty\" % \"netty-codec-http2\" % NettyVersion\n```\n\n# idea\n`OMHS` is a library for creating routing on top of `Netty`, like [sinatra.rb](http://sinatrarb.com/).\n\n`OMHS` will execute `function` on every path match.\n\n`/foo/bar` -\u003e execute function.\n\nParams should be passed from the matched path to function as arguments.\n\n`/foo/:string: -\u003e {(s: String) =\u003e ...}`\n\nEvery function should return an object which should be possible to deserialize to HTTP-response.\n\nIn `OMHS` it is `Response`-type. For simple requests (fire-and-forget let's say) I use `CommonResponse`,\nfor streaming is `StreamResponse`.\n\nBut in everyday life, we do not work usually with objects like a simple string,\nnumber, most of the cases are get strings/numbers from a database, or a remote connection, these results probably are wrapped into scala `Future`, or `zio.Task`, or another IO-like structure.\nSo for compatibility with another's libraries,\nOMHS use the `AsyncResult` object to translate library-wrapped result to OMHS result.\nTherefore every function should return `AsyncResult` of `Response`.\n\n```scala\nget(\"test\" / uuid) ~\u003e {(uuid: UUID) =\u003e\n  AsyncResult.completed(CommonResponse.plain(s\"$uuid\".getBytes))\n}\n```\n\nTransform ZIO-Task to `AsyncResult`:\n\n```scala\nval value = zio.Runtime.default.unsafeRun(task) // task returns CommonResponse\nAsyncResult.completed(value)\n```\n\nPaths are described as method + url like structure: `/foo/bar/` +\nadditional helpers: `uuid`/`string`/`long`/and `regex`/ or full url-path matcher: `*`.\n\n`headers` and `cookies` do not participate in matching, just paths.\n\nFor deserializing `body`/`query` need to implement a deserialization strategy:\ntransform from raw string to necessary object.\n\n# Examples:\n\n[code](https://github.com/fntz/omhs/blob/master/src/main/scala/MyApp.scala)\n\n[code based on zio.Task](https://github.com/truerss/truerss/blob/master/src/main/scala/truerss/api/SourcesApi.scala)\n\n# Using\n\n### Basic\n\n```scala\nimport com.github.fntz.omhs.RoutingDSL._\nimport com.github.fntz.omhs.AsyncResult\nimport AsyncResult.Implicits._ // useful implicits for string/futures\n// simple methods\nget(string / \"test\" / uuid) ~\u003e { (x: String, u: UUID) =\u003e \n  \"done\"  \n}\n\n// * \nget(\"test\" / *) ~\u003e {(xs: List[String]) =\u003e \n  \"done\"\n}\n\n// alternative syntax\nget(\"foo\" | \"bar\") ~\u003e { (choice: String) =\u003e\n  \"done\"  \n}\n```\n\n### Headers/Cookies syntax \n\n```scala\n// headers / cookies \npost(\"test\" \u003c\u003c header(\"User-Agent\") \u003c\u003c cookie(\"name\") \u003c\u003c header(\"Accept\")) \n  ~\u003e {(userAgent: String, name: String, accept: String)} =\u003e {\n  \"done\"  \n}\n```\n\n### Query Readers\n\nfor reading Query part (`?foo=bar`) need to implement `QueryReader`: \n\n```scala\ncase class SearchQuery(query: String) \nimplicit val querySearchReader = new QueryReader[SearchQuery] {\n  override def read(queries: Map[String, Iterable[String]]): Option[SearchQuery] = {\n    queries.get(\"query\").flatMap(_.headOption).map(SearchQuery)\n  }\n}\n```\n\n```scala\n// queries\n\n(\"test\" :? query[SearchQuery]) ~\u003e {(q: SearchQuery) =\u003e \"done\" }\n```\n\n### Read Body\n\nfor reading body from current request need to implement `BodyReader`. \n\n```scala\ncase class Person(id: Int)\nimplicit val personBodyReader = new BodyReader[Person] {\n  override def read(str: String): Person = ???\n}\n```\n\nand then:\n\n```scala\npost(\"test\" \u003c\u003c\u003c body[Person]) ~\u003e { (p: Person) ~\u003e \"done\" }\n```\n\nCurently the project supports [play-json](https://github.com/playframework/play-json),  \n[circle](https://github.com/circe/circe), and [jsoniter](https://github.com/plokhotnyuk/jsoniter-scala)\n\n### Files\n\n```scala\nimport io.netty.handler.codec.http.multipart.FileUpload\n\npost(\"test\" \u003c\u003c\u003c file) ~\u003e {(f: List[FileUpload]) =\u003e \"done\"}\n```\n\n### Access to the Current Request\n\n```scala\nget(string / \"test\") ~\u003e { (s: String, request: CurrentHttpRequest) =\u003e \n  \"done\"\n}\n```\n\n### Streaming\n\n```scala \nimport com.github.fntz.omhs.streams.ChunkedOutputStream\nimport AsyncResult.Streaming._ // \u003c- from stream to AsyncResult\nget(\"steaming\") ~\u003e { (stream: ChunkedOutputStream) =\u003e \n    stream.write(\"123\".getBytes())\n    stream.write(\"456\".getBytes())\n    stream.write(\"789\".getBytes())\n    // or with \u003c\u003c \n    stream \u003c\u003c \"000\"\n    stream  \n}\n```\n\nNote: ChunkedOutputStream must be last or penultimate argument:\n\n`(stream: ChunkedOutputStream, req: CurrentHttpRequest) =\u003e` or `(req: CurrentHttpRequest, stream: ChunkedOutputStream) =\u003e` is valid.\n\n\n### From Scala Future to AsyncResponse\n\n```scala\n\nimplicit val ec = executor\n\nimport com.github.fntz.omhs.AsyncResult.Implicits._ \n\nget(\"persons\" / long) ~\u003e {(id: Long) =\u003e \n  Future(DB.persons.byId(id))      // as example\n}\n\n```\n\n# Moar? sinatra like dsl\n\n```scala \nimport moar._ \n\nval rule = get(\"test\" / string) ~\u003e route { (x: String) =\u003e \n  if (x == \"foo\") {\n    implicit val enc = ServerCookieEncoder.STRICT // need for setting cookie\n    status(200)\n    setHeader(\"foo\", \"bar\")\n    setCookie(\"asd\", \"qwe\")\n    val c = new DefaultCookie(\"a\", \"b\")\n    c.setDomain(\"example.com\")\n    setCookie(c)\n    setHeader(\"x-header\", \"v-value\")\n    contentType(\"apllication/custom-type\")\n    \"done\" \n  } else {\n    status(400)\n    setHeader(\"y-header\", \"y-value\")\n    contentType(\"application/custom-another-type\")\n    \"not-found\"\n  }\n}\n\n// available functions are:\n- contentType\n- status\n- setCookie\n- setHeader\n\n```\n\n### Error handling in application\n\n```scala\nval route = new Route().addRules(r1, r2, r3).onUnhandled {\n    case PathNotFound(p) =\u003e\n      CommonResponse.json(404, s\"$p not found\")\n    case _ =\u003e\n      CommonResponse.json(500, \"boom\")\n  }\n```\n\n#### reasons are:\n\n```\n+ PathNotFound(path: String)\n+ QueryIsUnparsable(params: Map[String, Iterable[String]])\n+ CookieIsMissing(cookieName: String)\n+ HeaderIsMissing(headerName: String)\n+ BodyIsUnparsable(ex: Throwable)\n+ FilesIsUnparsable(ex: Throwable)\n+ UnhandledException(ex:  Throwable)\n```\n\n### Options:\n\n```scala\nval customSetup = Setup(\n  timeFormatter = DateTimeFormatter.RFC_1123_DATE_TIME\n    .withZone(ZoneOffset.UTC).withLocale(Locale.US),\n  sendServerHeader = false,\n  cookieDecoderStrategy = CookieDecoderStrategy.Lax,\n  maxContentLength = 512*1024,\n  enableCompression = false,\n  chunkSize = 1000,\n  isSupportHttp2 = true\n)\n```\n\n\n### Run server\n\n```scala \nval rule1 = ...\nval rule2 = ...\nval rule3 = ...\n\nval route = new Route().addRules(rule1, rule2, rule3)\nval server = OMHSServer.init(9000, route.toHandler)\n\n// you can change ServerBootstrap\nval server = OMHSServer.run(\n    port = 9000, \n    handler = route.toHandler,\n    sslContext = Some(OMHSServer.getJdkSslContext),\n    serverBootstrapChanges = (s: ServerBootstrap) =\u003e {\n        s.options(...).childOptions(...)\n    }\n  )\n\n// then \n\nserver.start()  \n  \n// and stop programmatically\n\nserver.stop()  \n\n```\n\n# check codegen: \npass `-Domhs.logLevel=verbose|info|none` to sbt/options\n\n### TODO\n\n* more settings/swagger\n\n\n# License: MIT\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffntz%2Fomhs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffntz%2Fomhs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffntz%2Fomhs/lists"}