{"id":24435278,"url":"https://github.com/igrishaev/taggie","last_synced_at":"2025-04-29T09:53:03.650Z","repository":{"id":270592722,"uuid":"910576285","full_name":"igrishaev/taggie","owner":"igrishaev","description":"Can we gain anything from Clojure tags?","archived":false,"fork":false,"pushed_at":"2025-03-07T15:04:52.000Z","size":113,"stargazers_count":18,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-15T05:02:43.427Z","etag":null,"topics":["clojure","edn","reader","tags"],"latest_commit_sha":null,"homepage":"https://github.com/igrishaev/taggie","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"unlicense","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/igrishaev.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-12-31T17:27:35.000Z","updated_at":"2025-04-07T13:23:28.000Z","dependencies_parsed_at":"2025-01-01T17:17:52.845Z","dependency_job_id":"e79b48db-482f-48a1-aba6-76f503ccb131","html_url":"https://github.com/igrishaev/taggie","commit_stats":null,"previous_names":["igrishaev/taggie"],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Ftaggie","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Ftaggie/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Ftaggie/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Ftaggie/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/igrishaev","download_url":"https://codeload.github.com/igrishaev/taggie/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251480032,"owners_count":21596015,"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":["clojure","edn","reader","tags"],"created_at":"2025-01-20T17:18:56.482Z","updated_at":"2025-04-29T09:53:03.591Z","avatar_url":"https://github.com/igrishaev.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Taggie\n\nAn experimental library trying find an answer to a strange question: is it\npossible to benefit from Clojure tags and readers, and how?\n\n**Table of Contents**\n\n\u003c!-- toc --\u003e\n\n- [WTF](#wtf)\n- [Installation and Usage](#installation-and-usage)\n- [EDN Support](#edn-support)\n- [Supported Types](#supported-types)\n- [Adding Your Types](#adding-your-types)\n- [Misc](#misc)\n\n\u003c!-- tocstop --\u003e\n\n## WTF\n\nTaggie extends printing methods such that types that could not be read from\ntheir representation now **can** be read. A quick example: if you print an atom,\nyou'll get a weird string:\n\n~~~clojure\n(atom 42)\n#\u003cAtom@7fea5978: 42\u003e\n~~~\n\nRun that string, and REPL won't understand you:\n\n~~~clojure\n#\u003cAtom@7fea5978: 42\u003e\nSyntax error reading source at (REPL:962:5).\nUnreadable form\n~~~\n\nBut with Taggie, it goes this way:\n\n~~~clojure\n(atom 42)\n#atom 42 ;; represented with a tag\n~~~\n\nAnd vice versa:\n\n~~~clojure\n#atom 42 ;; run it in repl\n#atom 42 ;; the result\n~~~\n\nThe value is an atom indeed, you can check it:\n\n~~~clojure\n(deref #atom 42)\n42\n~~~\n\nTags can be nested. Let's try some madness:\n\n~~~clojure\n(def omg #atom #atom #atom #atom #atom #atom 42)\n\n(println omg)\n#atom #atom #atom #atom #atom #atom 42\n\n@@@@@@omg\n42\n~~~\n\nBut this is not only about atoms! Taggie extends many types, e.g. refs, native\nJava arrays, `File`, `URI`, `URL`, `Date`, `java.time.*` classes, and something\nelse. See the corresponding section below.\n\n## Installation and Usage\n\nAdd this to your project:\n\n~~~clojure\n;; lein\n[com.github.igrishaev/taggie \"0.1.1\"]\n\n;; deps\ncom.github.igrishaev/taggie {:mvn/version \"0.1.1\"}\n~~~\n\nThen import the core namespace:\n\n~~~clojure\n(ns com.acme.server\n  (:require\n    taggie.core))\n~~~\n\nNow type in the repl any of these:\n\n~~~clojure\n#LocalDate \"2025-01-01\"\n#Instant \"2025-01-01T23:59:59Z\"\n#File \"/path/to/a/file.txt\"\n#URL \"https://clojure.org\"\n#bytes [0x00 0xff]\n#ints [1 2 3]\n#floats [1 2 3]\n#ByteBuffer [0 1 2 3 4]\n...\n~~~\n\nEach expression gives an instance of a corresponding type: a `LocalDate`, an\n`Instance`, a `File`, etc... `#bytes`, `#ints` and similar produce native Java\narrays.\n\nYou can pass tagged values into functions as usual:\n\n~~~clojure\n(deref #atom 42)\n42\n\n(alength #longs [1 2 3])\n3\n~~~\n\nTo observe what happends under the hood, prepend your expression with a\nbacktick:\n\n~~~clojure\n`(alength #longs [1 2 3])\n\n(clojure.core/alength (taggie.readers/__reader-longs-edn [1 2 3]))\n~~~\n\nInternally, all tags expand into an invocation of an EDN reader. Namely, `#longs\nitems` becomes `(taggie.readers/__reader-longs-edn items)`, and when evaluated,\nit returs a native array of longs.\n\n## EDN Support\n\nTaggie provides functions to read and write EDN with tags. They live in the\n`taggie.edn` namespace. Use it as follows:\n\n~~~clojure\n(def edn-dump\n  (taggie.edn/write-string #atom {:test 1\n                                  :values #longs [1 2 3]\n                                  :created-at #LocalDate \"2025-01-01\"}))\n\n(println edn-dump)\n\n;; #atom {:test 1,\n;;        :values #longs [1, 2, 3],\n;;        :created-at #LocalDate \"2025-01-01\"}\n~~~\n\nIt produces a string with custom tags and data being pretty printed. Let's read\nit back:\n\n~~~clojure\n(taggie.edn/read-string edn-dump)\n\n#atom {:test 1,\n       :values #longs [1, 2, 3],\n       :created-at #LocalDate \"2025-01-01\"}\n~~~\n\nThe `write` function writes EDN into a destination which might be a file path, a\nfile, an output stream, a writer, etc:\n\n~~~clojure\n(taggie.edn/write (clojure.java.io/file \"data.edn\")\n                  {:test (atom (ref (atom :secret)))})\n~~~\n\nThe `read` function reads EDN from any kind of source: a file path, a file, in\ninput stream, a reader, etc. Internally, a source is transformed into the\n`PushbackReader` instance:\n\n~~~clojure\n(taggie.edn/read (clojure.java.io/file \"data.edn\"))\n\n{:test #atom #ref #atom :secret}\n~~~\n\nBoth `read` and `read-string` accept standard `clojure.edn/read` options,\ne.g. `:readers`, `:eof`, etc. The `:readers` map gets merged with a global map\nof custom tags.\n\n## Motivation\n\nAside from jokes, this library might save your day. I often see people dump data\ninto .edn files, and the data has atoms, regular expressions, exceptions, and\nother unreadable types:\n\n~~~clojure\n(spit \"data.edn\"\n      (with-out-str\n        (clojure.pprint/pprint\n          {:regex #\"foobar\"\n           :atom (atom 42)\n           :error (ex-info \"boom\" {:test 1})})))\n\n(println (slurp \"data.edn\"))\n\n{:regex #\"foobar\", :atom #\u003cAtom@4f7aa8aa: 42\u003e, :error #error {\n :cause \"boom\"\n :data {:test 1}\n :via\n [{:type clojure.lang.ExceptionInfo\n   :message \"boom\"\n   :data {:test 1}\n   :at [user$eval43373$fn__43374 invoke \"form-init6283045849674730121.clj\" 2248]}]\n :trace\n [[user$eval43373$fn__43374 invoke \"form-init6283045849674730121.clj\" 2248]\n  [user$eval43373 invokeStatic \"form-init6283045849674730121.clj\" 2244]\n  ;; truncated\n  [clojure.lang.AFn run \"AFn.java\" 22]\n  [java.lang.Thread run \"Thread.java\" 833]]}}\n~~~\n\nThis dump cannot be read back due to:\n\n1. unknown `#\"foobar\"` tag (EDN doesn't support regex);\n2. broken `#\u003cAtom@4f7aa8aa: 42\u003e` expression;\n3. unknown `#error` tag.\n\nBut with Taggie, the same data produces tagged fields that **can** be read back.\n\n## Supported Types\n\nIn alphabetic order:\n\n| Type                       | Example                                                           |\n|----------------------------|-------------------------------------------------------------------|\n| `java.nio.ByteBuffer`      | `#ByteBuffer [0 1 2]`                                             |\n| `java.util.Date`           | `#Date \"2025-01-06T14:03:23.819Z\"`                                |\n| `java.time.Duration`       | `#Duration \"PT72H\"`                                               |\n| `java.io.File`             | `#File \"/path/to/file.txt\"`                                       |\n| `java.net.InetAddress`     | `#InetAddress \"google.com\"`                                       |\n| `java.time.Instant`        | `#Instant \"2025-01-06T14:03:23.819994Z\"`                          |\n| `java.time.LocalDate`      | `#LocalDate \"2034-01-30\"`                                         |\n| `java.time.LocalDateTime`  | `#LocalDateTime \"2025-01-08T11:08:13.232516\"`                     |\n| `java.time.LocalTime`      | `#LocalTime \"20:30:56.928424\"`                                    |\n| `java.time.MonthDay`       | `#MonthDay \"--02-07\"`                                             |\n| `java.time.OffsetDateTime` | `#OffsetDateTime \"2025-02-07T20:31:22.513785+04:00\"`              |\n| `java.time.OffsetTime`     | `#OffsetTime \"20:31:39.516036+03:00\"`                             |\n| `java.time.Period`         | `#Period \"P1Y2M3D\"`                                               |\n| `java.nio.file.Path`       | `#Path \"/path/to/some/file.txt\"`                                  |\n| `java.net.URI`             | `#URI \"foobar://test.com/path?foo=1\"`                             |\n| `java.net.URL`             | `#URL \"https://clojure.org\"`                                      |\n| `java.time.Year`           | `#Year \"2025\"`                                                    |\n| `java.time.YearMonth`      | `#YearMonth \"2025-02\"`                                            |\n| `java.time.ZoneId`         | `#ZoneId \"Europe/Paris\"`                                          |\n| `java.time.ZoneOffset`     | `#ZoneOffset \"-08:00\"`                                            |\n| `java.time.ZonedDateTime`  | `#ZonedDateTime \"2025-02-07T20:32:33.309294+01:00[Europe/Paris]\"` |\n| `clojure.lang.Agent`       | `#agent {:some \"data\"}`                                           |\n| `clojure.lang.Atom`        | `#atom {:inner 'state}`                                           |\n| `boolean[]`                | `#booleans [true false]`                                          |\n| `byte[]`                   | `#bytes [1 2 3]`                                                  |\n| `char[]`                   | `#chars [\\a \\b \\c]`                                               |\n| `double[]`                 | `#doubles [1.1 2.2 3.3]`                                          |\n| `Throwable-\u003emap`           | `#error \u003cresult of Throwable-\u003emap\u003e` (see below)                   |\n| `float[]`                  | `#floats [1.1 2.2 3.3]`                                           |\n| `int[]`                    | `#ints [1 2 3]`                                                   |\n| `long[]`                   | `#longs [1 2 3]`                                                  |\n| `clojure.lang.Namespace`   | `#ns com.acme.server`                                             |\n| `Object[]`                 | `#objects [\"test\" :foo 42 #atom false]`                           |\n| `clojure.lang.Ref`         | `#ref {:test true}`                                               |\n| `java.util.regex.Pattern`  | `#regex \"vesion: \\d+\"`                                            |\n| `clojure.lang.Volatile`    | `#volatile {:the \"state\"}`                                        |\n| `java.sql.Timestamp`       | `#sql/Timestamp \"2025-01-06T14:03:23.819Z\"`                       |\n\nThe `ns` tag, when reading, tries to find a namespace using `find-ns`. This\nfunction won't throw an exception when the namespace is missing; the result will\nbe nil.\n\nThe `#error` tag is a bit special: it returns a value with no parsing. It\nprevents an error when reading the result of printing of an exception:\n\n~~~clojure\n(println (ex-info \"boom\" {:test 123}))\n\n#error {\n :cause boom\n :data {:test 123}\n :via\n [{:type clojure.lang.ExceptionInfo\n   :message boom\n   :data {:test 123}\n   :at [taggie.edn$eval9263 invokeStatic form-init2367470449524935680.clj 97]}]\n :trace\n [[taggie.edn$eval9263 invokeStatic form-init2367470449524935680.clj 97]\n  [taggie.edn$eval9263 invoke form-init2367470449524935680.clj 97]\n  ;; truncated\n  [java.lang.Thread run Thread.java 833]]}\n~~~\n\nWhen reading such data from EDN with Taggie, you'll get a regular map.\n\n## Adding Your Types\n\nImagine you have a custom type and you want Taggie to hande it:\n\n~~~clojure\n(deftype SomeType [a b c])\n\n(def some-type\n  (new SomeType (atom :test)\n                (LocalDate/parse \"2023-01-03\")\n                (long-array [1 2 3])))\n~~~\n\nTo override the way it gets printed, run the `defprint` macro:\n\n~~~clojure\n(taggie.print/defprint SomeType ^SomeType some-type writer\n  (let [a (.-a some-type)\n        b (.-b some-type)\n        c (.-c some-type)]\n    (.write writer \"#SomeType \")\n    (print-method [a b c] writer)))\n~~~\n\nThe first argument is a symbol bound to a class. The second is a symbol bound to\nthe instance of this class (in some cases you'll need a type hint). The third\nsymbol is bound to the `Writer` instance. Inside the macro, you `.write` certain\nvalues into the writer. Avobe, we write the leading `\"#SomeType \"` string, and a\nvector of fields `a`, `b` and `c`. Calling `print-method` guarantees that all\nnested data will be written with their custom tags.\n\nNow if you print `some-type` or dump it into EDN, you'll get:\n\n~~~clojure\n#SomeType [#atom :test #LocalDate \"2023-01-03\" #longs [1 2 3]]\n~~~\n\nThe opposite step: define readers for `SomeType` class:\n\n~~~clojure\n(taggie.readers/defreader SomeType [vect]\n  (let [[a b c] vect]\n    (new SomeType a b c)))\n~~~\n\nIt's quite simple: the vector of fields is already parsed, so you only need to\nsplit it and pass fields into the constructor.\n\nThe `defreader` mutates a global map of EDN readers. When you read an EDN\nstring, the `SomeType` will be held. But it won't work in REPL: for example,\nrunning `#SomeType [...]` in REPL will throw an error. The thing is, REPL\nreaders cannot be overriden in runtime.\n\nBut you can declare your own readers: in `src` directory, create a file called\n`data_readers.clj` with a map:\n\n~~~clojure\n{SomeType some.namespace/__reader-SomeType-clj}\n~~~\n\nRestart the REPL, and now the tag will be available.\n\nAs you might have guessed, the `defreader` macro creates two functions:\n\n- `__reader-\u003ctag\u003e-clj` for a REPL reader;\n- `__reader-\u003ctag\u003e-edn` for an EDN reader.\n\nEach `-clj` reader relies on a corresponding `-edn` reader internally.\n\n**Emacs \u0026 Cider caveat:** I noticed that `M-x cider-ns-refresh` command ruins\nloading REPL tags. After this command being run, any attempt to execute\nsomething like `#LocalDate \"...\"` ends up with an error saying \"unbound\nfunction\". Thus, if you use Emacs and Cider, avoid this command.\n\n## Misc\n\n~~~\n©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©\nIvan Grishaev, 2025. © UNLICENSE ©\n©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©\n~~~\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Figrishaev%2Ftaggie","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Figrishaev%2Ftaggie","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Figrishaev%2Ftaggie/lists"}