{"id":18412421,"url":"https://github.com/roti/lut","last_synced_at":"2025-04-12T23:46:07.444Z","repository":{"id":57722484,"uuid":"243771264","full_name":"roti/lut","owner":"roti","description":"A library for data modeling in Scala.","archived":false,"fork":false,"pushed_at":"2020-08-09T10:12:01.000Z","size":58,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-02-16T05:42:07.302Z","etag":null,"topics":["case-classes","data-model","data-modeling","data-modelling","scala"],"latest_commit_sha":null,"homepage":"","language":"Scala","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/roti.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":"2020-02-28T13:41:07.000Z","updated_at":"2021-08-19T02:19:17.000Z","dependencies_parsed_at":"2022-09-26T21:50:31.081Z","dependency_job_id":null,"html_url":"https://github.com/roti/lut","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roti%2Flut","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roti%2Flut/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roti%2Flut/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roti%2Flut/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/roti","download_url":"https://codeload.github.com/roti/lut/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248647258,"owners_count":21139081,"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":["case-classes","data-model","data-modeling","data-modelling","scala"],"created_at":"2024-11-06T03:41:44.929Z","updated_at":"2025-04-12T23:46:07.423Z","avatar_url":"https://github.com/roti.png","language":"Scala","readme":"# Lut\n\nLut is an attempt to make data modelling easier in Scala. The design is inspired by Clojure's `defrecord`.\n\nScala's case classes are problematic for modelling data. They don't compose and include optionality, leading to a proliferation of classes and a lot of conversions between them.\n\nLut strives to offer an alternative, which:\n* works with immutable values\n* allows composition by inheritance\n* allows partial data\n\nThe basic idea is to keep data in maps and access it in a (relatively) typesafe manner through interfaces. \nThis way it looks and behaves like a normal class, with statically declared data members, while the implementation is actually a dynamic immutable map.\n\n### Dependency\n\n```scala\nlibraryDependencies += \"com.github.roti\" %% \"lut\" % \"0.5\"\n```\n\n### Scala Versions\n\nSupported scala versions are 2.11, 2.12 and 2.13.\n\nFor scala 2.13 you need to enable macro annotations:\n```scala\nscalacOptions += \"-Ymacro-annotations\"\n```\n\nFor scala 2.12 and earlier you need to add the macro paradise compiler plugin to your project:\n```scala\naddCompilerPlugin(\"org.scalamacros\" %% \"paradise\" % \"2.1.1\")\n```\n\n### Usage\n\nBuild the data class as a trait extending `Record` and annotate it with `@record`. We'll call it a record from now on.\n\n```scala\nimport roti.lut.annotation.record\nimport roti.lut.Record\n\n@record\ntrait Employee extends Record {\n  def id: Long\n  def firstName: String\n  def lastName: String\n  def phoneNumber: Option[String]\n}\n```\n\nThe `@record` annotation transforms the trait with a macro providing implementations for the abstract methods, two `apply` methods in the companion object and update methods.\n\nNow `Employee` can be used as if it were a fully implemented class:\n\n```scala\nval data: Map[String, Any] = Map(\"id\" -\u003e 100, \"firstName\" -\u003e \"John\", \"lastName\" -\u003e \"Smith\")\nval employee = Employee(data)\nprintln(employee.firstName + \" \" + employee.lastName + \" \" + employee.phoneNumber )  //\"John Smith None\"\n\n//Each abstract method has an update method, with the same name, but accepting a parameter.\n//The update method returns a new modified instance of the record.\nval employee2 = employee.phoneNumber(Some(\"123\"))\nprintln(employee.firstName + \" \" + employee.lastName + \" \" + employee.phoneNumber )  //\"John Smith Some(123)\"\n\n//instances can be created either from a Map[String, Any] or from individual field values  \nval employee3 = Employee(id = 100, firstName = \"John\", lastName = \"Smith\", phoneNumber = None)\n\n//destructuring works\nval Employee(id, fName, lName, _) = employee3\nprintln(id + \" \" + fName + \" \" + lName)  //100 John Smith\n```\n\nThe data is stored as a `Map[String, Any]`, the generated implementations of the abstract methods just access the values from this map. \nThe map itself is available through `.data`. Other values which are not exposed by the traits methods are left untouched when modified versions are created: \n\n```scala\nval data: Map[String, Any] = Map(\"id\" -\u003e 100, \"firstName\" -\u003e \"John\", \"lastName\" -\u003e \"Smith\", \"foo\" -\u003e \"bar\")\nval employee = Employee(data)\nprintln(employee.data.get(\"foo\") )  //Some(\"bar\")\n\nval employee2 = employee.phoneNumber(Some(\"123\"))\nprintln(employee2.data.get(\"foo\") )  //Some(\"bar\")\n```\n\nNo conversions are done when getting values from the map, so the map is expected to have the correct types. \nIf that's not the case a runtime exception will occur. This is true also when the field is another record.\n\n```scala\nimport roti.lut.annotation.record\nimport roti.lut.Record\n\n@record\ntrait Employee extends Record {\n  def id: Long\n  def firstName: String\n  def lastName: String\n  def phoneNumber: Option[String]\n  def department: Department\n}\n\n@record\ntrait Department extends Record {\n  def name: String\n}\n\nval data = Map(\"id\" -\u003e 100, \"firstName\" -\u003e \"John\", \"lastName\" -\u003e \"Smith\", \"department\" -\u003e Map(\"name\" -\u003e \"sales\"))\nprintln(Employee(data).department)  //will throw an exception, because a Department is expected, but a Map is found\n```\n\nFor this case you can use the helper `Record.to` which will recursively convert maps to `Record` instances where needed:\n\n```scala\nval employee = Record.to[Employee](data)\nprintln(employee.department)  //now it works, Map(\"name\" -\u003e \"sales\") was converted to an instance of Department\n```\n\n\n### Partial information\n\nSince the underlying data is stored as a `Map`, and the `apply` method which creates instances from maps does not do any checks, it is possible to have instances with incomplete data. \nThis makes it possible to use the same class in situations where the data is gradually built, in multiple steps, by simply passing the instance around and creating modified versions (of course, as long you don't try to retrieve data which does not exist).\nFor example in a CRUD context, you can use the same trait for insert and update operations, where the only difference is that there is no id when doing insert. \nThe insert operation will receive an incomplete instance, where the id is missing (and will not try to retrieve the id), and will return a complete instance with the generated id.\n\n\n### Equality\n\nSince a record is meant to be just a convenient interface over a `Map`, equality is based on the underlying data. \nTwo record instances are equal if and only if the underlying maps are equal. \nIn other words, data which is not exposed through the record's interface, participates in the equality check.\n\nFurthermore records can be compared to plain maps and the semantic is the same: if the underlying map is equal to the map then the record is equal to the map.\n\n\n### Optionality\n\nThe usual approach for optionality in maps is not to include values in the map when they are missing (as opposed to including a representation of the missing value, like `null` or `None`).\nLut follows the same rule: when the type of a field is `Option` and the value is `None`, the underlying map does not contain any value for that field. \nIf the value is `Some(x)`, then the underlying map contains the value `x` for that field.\n\n```scala\n@record\ntrait Foo extends Record {\n  def bar: Option[Int]\n  def baz: Option[String]\n}\n\nval foo1 = Foo(bar = Some(10), baz = None)\nprintln(foo1.data)  //Map(bar -\u003e 10)\n```\n\nIn other words, the underlying map should never have values which are instances of `Option`, but rather the value should be present in the map or not.\n**You need to be aware of this rule when building the map yourself.**\n\n\n### Inheritance\n\nSince we work with traits, we can make use of inheritance:\n\n```scala\n@record\ntrait Audit extends Record {\n  def lastUpdatedAt: Long\n  def lastUpdatedBy: String\n}\n\n//no need to extend Record as well\n@record\ntrait Employee extends Audit {\n  def id: Long\n  def name: String\n}\n\n//inherited fields need to have a value as well\nval employee = Employee(id = 100, name = \"John Smith\", lastUpdatedAt = 1587812929, lastUpdatedBy = \"admin\")\n```\n\n\n### Other members\n\n`@record` can be used on both traits and abstract classes, and there are no restrictions on how the class or trait should look like. \nIt can have vals and normal methods, but they are not taken into consideration (which means they are not considered to be fields of the record).\nOf course, they can't have the name of one of the generated methods.\n\n\n\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froti%2Flut","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Froti%2Flut","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froti%2Flut/lists"}