{"id":16964991,"url":"https://github.com/propensive/kaleidoscope","last_synced_at":"2025-04-04T15:11:47.014Z","repository":{"id":29341829,"uuid":"121402901","full_name":"propensive/kaleidoscope","owner":"propensive","description":"Statically-checked inline matching on regular expressions in Scala","archived":false,"fork":false,"pushed_at":"2025-01-26T21:45:57.000Z","size":3685,"stargazers_count":166,"open_issues_count":2,"forks_count":8,"subscribers_count":11,"default_branch":"main","last_synced_at":"2025-03-28T20:49:18.995Z","etag":null,"topics":["capture-groups","pattern-matching","regex","regular-expression","scala"],"latest_commit_sha":null,"homepage":"https://soundness.dev/kaleidoscope/","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/propensive.png","metadata":{"files":{"readme":".github/readme.md","changelog":null,"contributing":".github/contributing.md","funding":null,"license":null,"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":"2018-02-13T15:59:37.000Z","updated_at":"2025-02-26T09:58:30.000Z","dependencies_parsed_at":"2023-02-19T17:00:25.607Z","dependency_job_id":"bf631094-8605-43c5-830d-88ba0a640d9c","html_url":"https://github.com/propensive/kaleidoscope","commit_stats":null,"previous_names":[],"tags_count":27,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/propensive%2Fkaleidoscope","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/propensive%2Fkaleidoscope/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/propensive%2Fkaleidoscope/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/propensive%2Fkaleidoscope/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/propensive","download_url":"https://codeload.github.com/propensive/kaleidoscope/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247198469,"owners_count":20900081,"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":["capture-groups","pattern-matching","regex","regular-expression","scala"],"created_at":"2024-10-13T23:44:47.188Z","updated_at":"2025-04-04T15:11:46.992Z","avatar_url":"https://github.com/propensive.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"[\u003cimg alt=\"GitHub Workflow\" src=\"https://img.shields.io/github/actions/workflow/status/propensive/kaleidoscope/main.yml?style=for-the-badge\" height=\"24\"\u003e](https://github.com/propensive/kaleidoscope/actions)\n[\u003cimg src=\"https://img.shields.io/discord/633198088311537684?color=8899f7\u0026label=DISCORD\u0026style=for-the-badge\" height=\"24\"\u003e](https://discord.com/invite/MBUrkTgMnA)\n\u003cimg src=\"/doc/images/github.png\" valign=\"middle\"\u003e\n\n# Kaleidoscope\n\n__Statically-typed inline pattern matching on regular expressions__\n\n__Kaleidoscope__ is a small library to make pattern matching against strings more\npleasant. Regular expressions can be written directly in patterns, and\ncapturing groups bound directly to variables, typed according to the group's\nrepetition. Here is an example:\n```scala\ncase class Email(user: Text, domain: Text)\n\nemail match\n  case r\"$user([^@]+)@$domain(.*)\" =\u003e Email(name, domain)\n```\n\nStrings are widely used to carry complex data, when it's wiser to use\nstructured objects. Kaleidoscope makes it easier to move away from strings.\n\n## Features\n\n- pattern match strings against regular expressions\n- regular expressions can be written inline in patterns, anywhere a string could match\n- direct extraction of capturing groups in patterns\n- typed extraction (into `List`s or [Vacuous](https://github.com/propensive/vacuous/) `Optional`s) of variable-length capturing groups\n- static checking of regular expression syntax\n- simpler \"glob\" syntax is also provided\n\n\n## Availability\n\nKaleidoscope is available as a binary for Scala 3.4.0 and later, from [Maven\nCentral](https://central.sonatype.com). To include it in an `sbt` build, use\nthe coordinates:\n```scala\nlibraryDependencies += \"dev.soundness\" % \"kaleidoscope-core\" % \"0.1.0\"\n```\n\n\n\n\n\n\n\n## Getting Started\n\nKaleidoscope is included in the `kaleidoscope` package, and exported to the\n`soundness` package.\n\nTo use Kaleidoscope alone, you can include the import,\n```scala\nimport kaleidoscope.*\n```\nor to use it with other [Soundness](https://github.com/propensive/soundness/) libraries, include:\n```scala\nimport soundness.*\n```\n\nNote that Kaleidoscope uses the `Text` type from\n[Anticipation](https://github.com/propensive/anticipation) and the `Optional`\ntype from [Vacuous](https://github.com/propensive/vacuous/). These offer some\nadvantages over `String` and `Option`, and they can be easily converted:\n`Text#s` converts a `Text` to a `String` and `Optional#option` converts an\n`Optional` value to its equivalent `Option`. The necessary imports are shown in\nthe examples.\n\nYou can then use a Kaleidoscope regular expression—a string prefixed with\nthe letter `r`—anywhere you can pattern match against a string in Scala. For example,\n```scala\nimport anticipation.Text\n\ndef describe(path: Text): Unit =\n  path match\n    case r\"/images/.*\" =\u003e println(\"image\")\n    case r\"/styles/.*\" =\u003e println(\"stylesheet\")\n    case _             =\u003e println(\"something else\")\n```\nor,\n```scala\nimport vacuous.{Optional, Unset}\n\ndef validate(email: Text): Optional[Text] = email match\n  case r\"^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,6}$$\" =\u003e email\n  case _                                            =\u003e Unset\n```\n\nSuch patterns will either match or not, however should they match, it is\npossible to extract parts of the matched string using _capturing groups_. The\npattern syntax is exactly as described in the [Java Standard\nLibrary](https://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html),\nwith the exception that a capturing group (enclosed within `(` and `)`) may be\nbound to an identifier by placing it, like an interpolated string substitution,\nimmediately before to the capturing group, as `$identifier` or `${identifier}`.\n\nHere is an example of using a pattern match against filenames:\n```scala\nenum FileType:\n  case Image(text: Text)\n  case Stylesheet(text: Text)\n\ndef identify(path: Text): FileType = path match\n  case r\"/images/${img}(.*)\"  =\u003e FileType.Image(img)\n  case r\"/styles/$styles(.*)\" =\u003e FileType.Stylesheet(styles)\n```\n\nAlternatively, as with patterns in general, these can be extracted directly in a\n`val` definition (though this is common usage).\n\nHere is an example of matching an email address:\n```scala\nval r\"^[a-z0-9._%+-]+@$domain([a-z0-9.-]+\\.$tld([a-z]{2,6}))$$\" =\n  \"test@example.com\": @unchecked\n```\n\nThe `@unchecked` annotation ascribed to the result is standard Scala, and\nacknowledges to the compiler that the match is _partial_ and may fail at\nruntime.\n\nIf you try this example in the Scala REPL, it would bind the following values:\n```\n\u003e domain: Text = t\"example.com\"\n\u003e tld: Text = t\"com\"\n```\n\nIn addition, the syntax of the regular expression will be checked at\ncompile-time, and any issues will be reported then.\n\n### Repeated and optional capture groups\n\nA normal, _unitary_ capturing group, like `domain` and `tld` above, will\nextract into `Text` values. But if a capturing group has a repetition suffix,\nsuch as `*` or `+`, then the extracted type will be a `List[Text]`. This also\napplies to repetition ranges, such as `{3}` (exactly three times), `{2,}`\n(at least twice) or `{1,9}` (at least once, and at most nine times). Note that `{1}`\nwill still extract a `Text` value, not a `List[Text]`.\n\nThe type of each captured group is determined\nstatically from the pattern, and not dynamically from the runtime scrutinee.\n\nA capture group may be marked as optional, meaning it can appear either zero or\none times. This will extract a value with the type `Optional[Text]`; that is,\nif it present it will be a `Text` value, and if not, it will be `Unset`.\n\nFor example, see how `init` is extracted as a `List[Text]`, below:\n```scala\nimport gossamer.{skip, Rtl}\n\ndef parseList(): List[Text] = \"parsley, sage, rosemary, and thyme\" match\n  case r\"$only([a-z]+)\"                      =\u003e List(only)\n  case r\"$first([a-z]+) and $second([a-z]+)\" =\u003e List(first, second)\n  case r\"$init([a-z]+, )*and $last([a-z]+)\"  =\u003e init.map(_.skip(2, Rtl)) :+ last\n```\n\n### Escaping\n\nEscaping happens at two levels between source code and regular expression.\nFirst when source code is interpreted as a string. And again when that string\nis interpreted as a regular expression pattern.\n\nThis is particularly apparent when pattern matching a single backslash (`\\`) in Java:\nwe must write `java.util.regex.Pattern.compile(\"\\\\\\\\\")`. The backslash in the\nregular expression needs to be escaped with another backslash; then _each_ of those\nbackslashes must be escaped in order to embed it in a string.\n\nThe situation is improved in Kaleidoscope patterns, written as single- (`r\"...\"`)\nor triple-quoted (`r\"\"\"...\"\"\"`) interpolated strings: special characters,\nnotably `\\`, do _not_ need to be escaped. This means that only the regular expression\nescaping rules need to be considered.\n\nAn exception is `$` (which is used to indicate a substitution) and should be\nwritten as `$$`.\n\nIt is still necessary, however, to follow the regular expression escaping\nrules, for example, an extractor matching a single opening parenthesis would be\nwritten as `r\"\\(\"` or `r\"\"\"\\(\"\"\"`.\n\n### `Regex` values\n\nRegular expressions may be defined as values outside of pattern matches, using the\nsame syntax. For example,\n```scala\nval IpAddress = r\"([0-9]+\\.){3}[0-9]+\"\n```\nwhich may then be used in a pattern,\n```scala\ninput match\n  case IpAddress(addr) =\u003e addr\n```\nbut can only represent an entire match, and cannot extract capturing groups.\n\nSuch values are instances of `Regex` and provide access to the source pattern as a\n`Text` value, `Regex#pattern`, as well as the position and nature of any groups\nwithin the pattern.\n\n`Regex`s are used in other Soundness libraries wherever a regular expression is\nrequired. More importantly, `Text` is _never_ used for a value that\nrepresents a regular expression. So it's possible to know from a method's\nsignature whether its parameter is interpreted as a regular expression or as\na direct string.\n\nIndeed, in Gossamer, the method `sub` (for making substitutions in a textual\nvalue) is overloaded to take either a `Regex` or a `Text` parameter, and to behave\naccordingly.\n\n## Globs\n\nGlobs offer a simplified but limited form of regular expression. You can use\nthese in exactly the same way as a standard regular expresion, using the\n`g\"...\"` interpolator instead.\n\nFor example,\n```scala\npath match\n  case g\"/usr/local/bin/$name\"     =\u003e name\n  case g\"/home/*/.local/bin/$name\" =\u003e name\n```\n\nThe appearance of a `*` in a glob will match any sequence of characters,\nexcept for `/` and `\\`. A `?` will match exactly one character. An extractor,\nsuch as `$name` above, is equivalent to `*`, but binds the value to an identifier.\n\nAs an expression (rather than a pattern), and interpolated string, `g\"\"`, will\nalso produce a `Regex` value, and can be used anywhere a `Regex` is valid. In\nfact, globs are implemented as a simpler front-end to regular expressions. So it\nwould be possible to write, `path.sub(g\"/home/*/.local\", t\"/usr/local\")` _or_\n`path.sub(r\"/home/[^/]*/.local\", t\"/usr/local\")` to achieve the same goal.\n\n### Equality\n\nIt is even possible, sometimes, to equate regular expressions and globs. For\nexample, `g\".local/*\" == r\"\\.local/[^/\\\\]*\"` returns `true` because they are\nrepresented by identical underlying patterns. However, the inequality of two\n`Regex` instances does not necessarily indicate any difference in the behavior\nof two `Regex`s: it may be impossible to find any input where one matches and the\nother does not, while they are implemented differently. (As a trivial example,\nconsider `r\"[xy]\"` and `r\"[yx]\"`: non-equal `Regex`s, with identical behavior.)\n\n\n## Status\n\nKaleidoscope is classified as __maturescent__. For reference, Soundness projects are\ncategorized into one of the following five stability levels:\n\n- _embryonic_: for experimental or demonstrative purposes only, without any guarantees of longevity\n- _fledgling_: of proven utility, seeking contributions, but liable to significant redesigns\n- _maturescent_: major design decisions broady settled, seeking probatory adoption and refinement\n- _dependable_: production-ready, subject to controlled ongoing maintenance and enhancement; tagged as version `1.0.0` or later\n- _adamantine_: proven, reliable and production-ready, with no further breaking changes ever anticipated\n\nProjects at any stability level, even _embryonic_ projects, can still be used,\nas long as caution is taken to avoid a mismatch between the project's stability\nlevel and the required stability and maintainability of your own project.\n\nKaleidoscope is designed to be _small_. Its entire source code currently consists\nof 681 lines of code.\n\n## Building\n\nKaleidoscope will ultimately be built by Fury, when it is published. In the\nmeantime, two possibilities are offered, however they are acknowledged to be\nfragile, inadequately tested, and unsuitable for anything more than\nexperimentation. They are provided only for the necessity of providing _some_\nanswer to the question, \"how can I try Kaleidoscope?\".\n\n1. *Copy the sources into your own project*\n   \n   Read the `fury` file in the repository root to understand Kaleidoscope's build\n   structure, dependencies and source location; the file format should be short\n   and quite intuitive. Copy the sources into a source directory in your own\n   project, then repeat (recursively) for each of the dependencies.\n\n   The sources are compiled against the latest nightly release of Scala 3.\n   There should be no problem to compile the project together with all of its\n   dependencies in a single compilation.\n\n2. *Build with [Wrath](https://github.com/propensive/wrath/)*\n\n   Wrath is a bootstrapping script for building Kaleidoscope and other projects in\n   the absence of a fully-featured build tool. It is designed to read the `fury`\n   file in the project directory, and produce a collection of JAR files which can\n   be added to a classpath, by compiling the project and all of its dependencies,\n   including the Scala compiler itself.\n   \n   Download the latest version of\n   [`wrath`](https://github.com/propensive/wrath/releases/latest), make it\n   executable, and add it to your path, for example by copying it to\n   `/usr/local/bin/`.\n\n   Clone this repository inside an empty directory, so that the build can\n   safely make clones of repositories it depends on as _peers_ of `kaleidoscope`.\n   Run `wrath -F` in the repository root. This will download and compile the\n   latest version of Scala, as well as all of Kaleidoscope's dependencies.\n\n   If the build was successful, the compiled JAR files can be found in the\n   `.wrath/dist` directory.\n\n## Contributing\n\nContributors to Kaleidoscope are welcome and encouraged. New contributors may like\nto look for issues marked\n[beginner](https://github.com/propensive/kaleidoscope/labels/beginner).\n\nWe suggest that all contributors read the [Contributing\nGuide](/contributing.md) to make the process of contributing to Kaleidoscope\neasier.\n\nPlease __do not__ contact project maintainers privately with questions unless\nthere is a good reason to keep them private. While it can be tempting to\nrepsond to such questions, private answers cannot be shared with a wider\naudience, and it can result in duplication of effort.\n\n## Author\n\nKaleidoscope was designed and developed by Jon Pretty, and commercial support and\ntraining on all aspects of Scala 3 is available from [Propensive\nO\u0026Uuml;](https://propensive.com/).\n\n\n\n## Name\n\nKaleidoscope is named after the optical instrument which shows pretty patterns to its user, while the library also works closely with patterns.\n\nIn general, Soundness project names are always chosen with some rationale,\nhowever it is usually frivolous. Each name is chosen for more for its\n_uniqueness_ and _intrigue_ than its concision or catchiness, and there is no\nbias towards names with positive or \"nice\" meanings—since many of the libraries\nperform some quite unpleasant tasks.\n\nNames should be English words, though many are obscure or archaic, and it\nshould be noted how willingly English adopts foreign words. Names are generally\nof Greek or Latin origin, and have often arrived in English via a romance\nlanguage.\n\n## Logo\n\nThe logo is a loose allusion to a hexagonal pattern, which could appear in a kaleidoscope.\n\n## License\n\nKaleidoscope is copyright \u0026copy; 2025 Jon Pretty \u0026 Propensive O\u0026Uuml;, and\nis made available under the [Apache 2.0 License](/license.md).\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpropensive%2Fkaleidoscope","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpropensive%2Fkaleidoscope","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpropensive%2Fkaleidoscope/lists"}