{"id":13760426,"url":"https://github.com/Workiva/morphe","last_synced_at":"2025-05-10T10:32:44.917Z","repository":{"id":62432092,"uuid":"150485367","full_name":"Workiva/morphe","owner":"Workiva","description":"A Clojure utility for defining and applying aspects to function definitions.","archived":true,"fork":false,"pushed_at":"2019-04-25T20:51:44.000Z","size":181,"stargazers_count":25,"open_issues_count":0,"forks_count":2,"subscribers_count":40,"default_branch":"master","last_synced_at":"2024-08-03T13:04:51.715Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Workiva.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-09-26T20:20:39.000Z","updated_at":"2023-07-31T18:13:54.000Z","dependencies_parsed_at":"2022-11-01T21:00:44.887Z","dependency_job_id":null,"html_url":"https://github.com/Workiva/morphe","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Workiva%2Fmorphe","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Workiva%2Fmorphe/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Workiva%2Fmorphe/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Workiva%2Fmorphe/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Workiva","download_url":"https://codeload.github.com/Workiva/morphe/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224949812,"owners_count":17397239,"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":[],"created_at":"2024-08-03T13:01:10.015Z","updated_at":"2024-11-16T17:31:06.597Z","avatar_url":"https://github.com/Workiva.png","language":"Clojure","funding_links":[],"categories":["Clojure"],"sub_categories":[],"readme":"# Morphe (μορφή) [![Clojars Project](https://img.shields.io/clojars/v/com.workiva/morphe.svg)](https://clojars.org/com.workiva/morphe) [![CircleCI](https://circleci.com/gh/Workiva/morphe/tree/master.svg?style=svg)](https://circleci.com/gh/Workiva/morphe/tree/master)\n\n\u003e \"Thus if we regard objects independently of their attributes and investigate any aspect of them as so regarded, we shall not be guilty of any error on this account, any more than when we draw a diagram on the ground and say that a line is a foot long when it is not; because the error is not in the premises. The best way to conduct an investigation in every case is to take that which does not exist in separation and consider it separately; which is just what the arithmetician or the geometrician does.\"\n\u003e \n\u003e Aristotle, *Metaphysics*\n\n---\n\n\u003c!-- toc --\u003e\n\n- [Overview](#overview)\n- [How does it work?](#how-does-it-work)\n- [API Documentation](#api-documentation)\n- [Details](#details)\n- [`morphe.core` utilities](#morphecore-utilities)\n    + [`defn`](#defn)\n    + [`parse-defn: [\u0026form \u0026env name \u0026 fdecl]`](#parse-defn-form-env-name--fdecl)\n    + [`fn-form-\u003edefn: [fn-form]`](#fn-form-defn-fn-form)\n    + [`prefix-form: [fn-form expression]`](#prefix-form-fn-form-expression)\n    + [`alter-form: [fn-form expression]`](#alter-form-fn-form-expression)\n    + [`prefix-bodies: [fn-form expression]`](#prefix-bodies-fn-form-expression)\n    + [`alter-bodies: [fn-form expression]`](#alter-bodies-fn-form-expression)\n    + [`*warn-on-noop*`](#warn-on-noop)\n- [Examples](#examples)\n  * [Logging/tracing call sites](#loggingtracing-call-sites)\n  * [Tagging for metrics](#tagging-for-metrics)\n  * [Mix \u0026 match](#mix--match)\n  * [Macrotic Transformations](#macrotic-transformations)\n- [Maintainers and Contributors](#maintainers-and-contributors)\n  * [Active Maintainers](#active-maintainers)\n  * [Previous Contributors](#previous-contributors)\n\n\u003c!-- tocstop --\u003e\n\n## Overview\n\nGather round, and I shall tell you a fine tale. Once upon a time, there was a simple function in an API, a thin wrapper over more meaty code:\n\n```clojure\n(defn do-a-thing [x stuff] (.doThatThing x stuff))\n```\n\nBut a time came when we wanted to log every time it was called:\n\n```clojure\n(defn do-a-thing\n  [x stuff]\n  (log/trace \"calling function: app.api/do-a-thing\")\n  (.doThatThing x stuff))\n```\n\nOf course, we wanted to do the same with many functions in our codebase. This would lead to unnecessary code bloat, so we employed a standard and idiomatic solution that simultaneously:\n\n - reduced the amount of bureucratic code.\n - ensured that we could switch out clojure.tools/logging for another solution in all places at any time.\n - avoided bloating the call stack with unnecessary functional wrapping.\n\nThat is, we defined a new `defn`-like macro that would automatically generate the appropriate logging line. This was an improvement. We could replace the definition for `do-a-thing` and the many other logged functions with this simple line:\n\n```clojure\n(def-logged-fn do-a-thing [x stuff] (.doThatThing x stuff))\n```\n\nSoon after, we wanted to know how long each call to `do-a-thing` would take:\n\n```clojure\n(def do-a-thing-timer (metrics/timer \"Timer for the function: app.api/do-a-thing\"))\n\n(metrics/register metrics/DEFAULT\n                  [\"app.api\" \"do-a-thing\" \"timer\"]\n                  do-a-thing-timer)\n\n(def-logged-fn do-a-thing\n  [x stuff]\n  (let [context (.time timer)\n        result (.doThatThing x stuff)]\n    (.stop context)\n    result)))\n```\n\n\nAt first, all the functions we were logging were also functions we wanted to time, so we wrote a macro to generate all this code and let us go back to something simple, this time saving ourselves a few hundred lines of fragile copy-paste boilerplate:\n\n```clojure\n(def-logged-and-timed-fn do-a-thing [x stuff] (.doThatThing x stuff))\n```\n\nBut alas! Our needs still grew, and several things happened at once. We incorporated tracing into our codebase, and we no longer wished for all our logged functions to be timed, or for all our timed functions to be traced, or all our traced functions to be logged -- we wanted any combination of the three. The optimizations we'd made no longer applied, so our little one-line wrapper was up to twenty-seven lines. Even after applying other mitigation techniques, it was not ideal:\n\n```clojure\n(let [timer (metrics/timer \"Timer for the function: api.api/do-a-thing\")]\n  (metrics/register metrics/DEFAULT\n                    [\"api.api\" \"do-a-thing\" \"timer\"]\n                    timer)\n  (defn do-a-thing\n    [x stuff]\n    (log/trace \"calling function: app.api/do-a-thing\")\n    (metrics/with-timer timer\n      (tracing/with-tracing \"app.api/do-a-thing:[x stuff]\"\n        (.doThatThing x stuff))))))\n```\n\nMultiply this effect by the number of functions we apply telemetry to across our entire service. It is tedious, and contains a good deal of copy-paste code, fragile under inevitable future changes (for instance, swapping out a logging library or modifying the metrics implementation). Moreover, all that boilerplate is very distacting if you care about the business logic and nothing else. What it seemed we really *needed* was a full suite of `def-*-fn`s:\n\n - `def-logged-fn`\n - `def-traced-fn`\n - `def-timed-fn`\n - `def-logged-traced-fn`\n - `def-logged-timed-fn`\n - `def-traced-timed-fn`\n - `def-logged-traced-timed-fn`\n\nThat, of course, is ridiculous. Besides, with just one additional fourth axis, we'd need 15 of these. For n, 2\u003csup\u003en\u003c/sup\u003e-1.\n\nThe key to solving our problem once and for all was to recognize that these were all _completely independent_ [aspects](https://en.wikipedia.org/wiki/Aspect-oriented_programming) of a function definition. None of the manual transformations depended on any of the others. Thus was born `morphe`. Our one-liner could once again be a one-liner:\n\n```clojure\n(m/defn ^{::m/aspects [timed logged traced]} do-a-thing [x stuff] (.doThatThing x stuff))\n```\n\nAnd in case you are skeptical as to how this solves any problem in the first place, remember that the best predictor of bug count in a code base is the *size* of the code base. This library has a number of potential applications, but the easiest all involve removing boilerplate.\n\n## How does it work?\n\nIn Clojure's grammar, `defn` forms have many optional and variadic terms. In order to compile a function, Clojure's [`defn`](https://github.com/clojure/clojure/blob/clojure-1.9.0-alpha14/src/clj/clojure/core.clj#L283) first parses the definition you have written, then it writes an actual function form which is compiled.\n\nIn this library, we have forked `defn`, splitting it into its two fundamental components: the parser and writer. The parser outputs a `FnForm` record which is consumed by the writer. But between being parsed and being compiled, the FnForm record can easily be processed and modified by *aspect-defining* functions.\n\nIn the line above, `^{::m/aspects [...]}` tells Clojure's [reader](https://clojure.org/reference/reader) to attach a map of metadata to a symbol. `morphe.core/defn` parses the function definition as normal, then examines the symbol's metadata to determine which aspects it is tagged with. The library then calls the tagged aspect functions to modify the parsed form. Once all such tags have been applied, Morphe's `defn` passes the parsed form along to the writer, just as `clojure.core/defn` implicitly would have done.\n\nIt is fairly straightforward to modify the FnForm record. But `morphe` provides a number of conveniences to make writing common aspect transformations even simpler; for example, wrapping the whole definition (perhaps in the body of a `let`), or prefixing every body of the function (perhaps with generated log statements). For instance, defining a simple trace-level logging transformation is easy:\n\n```clojure\n(defn traced\n  \"Inserts a log/trace call as the first item in the fn body.\"\n  [fn-form]\n  (m/prefix-bodies fn-form\n                   `(log/trace \"calling function: \"\n                               ~(format \"%s/%s:%s\" (ns-name \u0026ns) \u0026name \u0026params)))))\n```\nThis is equivalent to the following method, which does not use any convenience functions and instead modifies the `FnForm` record directly:\n\n```clojure\n(defn traced\n  \"Inserts a log/trace call as the first item in the fn body.\"\n  [fn-form]\n  (let [namespaced-fn-name (format \"%s/$s\"\n                                   (str (ns-name (:namespace fn-form)))\n                                   (str (:fn-name fn-form)))]\n    (assoc fn-form :bodies\n           (for [[body args] (apply map list ((juxt :bodies :arglists) fn-form))]\n             (conj body `(log/trace ~(format \"calling function: %s:%s\"\n                                             namespaced-fn-name\n                                             args)))))))\n```\n\n## API Documentation\n\n[Clojure API documentation can be found here.](/documentation/index.html)\n\n## Details\n\nIf your use case is complex, you can modify the FnForm record directly. If you have never written [Clojure macros](https://clojure.org/reference/macros), there are a few tricky things to this process. The community is helpful, and help is also available in [book](https://www.braveclojure.com/writing-macros/) [form](https://www.amazon.com/Lisp-Advanced-Techniques-Common/dp/0130305529).\n\n## `morphe.core` utilities\n\nClojure's `defmacro` is an [anaphoric macro](https://en.wikipedia.org/wiki/Anaphoric_macro). Code inside `defmacro` has access to two special variables, `\u0026env` and `\u0026form`. `\u0026env` is \"a map of local bindings at the point of macro expansion. The env map is from symbols to objects holding compiler information about that binding.\" `\u0026form` is \"the actual form (as data) that is being invoked.\"\n\nMany of morphe's utilities follow this theme. Depending on the utility, some of the following variables are available:\n\n- `\u0026ns`: the namespace in which the aspect-modified function is being run.\n- `\u0026name`: the unqualified name given to the function.\n- `\u0026env-keys`: the *keyset* of the `\u0026env` map as seen by the `morphe.core/defn` macro itself (i.e., set of symbols bound in a local scope)\n- `\u0026meta`: the metadata with which the function has been tagged\n- `\u0026params`: the paramaters vector for *a particular* arity of the function.\n- `\u0026body`: the collection of expression(s) constituting *a particular* arity of the function.\n- `\u0026form`: an *uninspectable* representation of the collection of expressions for the entire function declaration; useful to wrap the whole `defn` with a lexical scope.\n\n#### `defn`\n\nA drop-in replacement for Clojure's `defn`. In the simple case, the two should be indistinguishable. But you can tag the fn-name with metadata, under the keyword `:morphe.core/aspects`, to trigger the application of aspects. `morphe.core/defn` first calls `parse-defn`, then applies the tagged aspects in order, then calls `fn-form=\u003edefn`.\n\n#### `parse-defn: [\u0026form \u0026env name \u0026 fdecl]`\n\nYou probably don't want to use this, but it's available if you do. This function contains half of the implementation of Clojure.core's `defn`. Intended to be used with `fn-form-\u003edefn`. It takes the same arguments as `defn`, but this returns a FnForm record containing the result of parsing the defn. `\u0026form` and `\u0026env` must be passed in explicitly: they are implicit arguments [available only inside macro definitions](https://clojure.org/reference/macros).\n\n#### `fn-form-\u003edefn: [fn-form]`\n\nAs with `parse-defn`, this is another low-level function you probably don't need. This function contains the second half of the implementation of Clojure.core's `defn`. Intended to be used with `parse-defn`. It takes a FnForm record as output by `parse-defn`, and returns a `(def ...)` form in the same manner as Clojure's `defn` macro.\n\n#### `prefix-form: [fn-form expression]`\n\nAnaphoric macro, providing `\u0026ns`, `\u0026name`, `\u0026env-keys`, and `\u0026meta`.\n\nThis will prefix the entire form with the provided expression. Example: \n\n```\n(prefix-form\n  fn-form\n  `(def gets-defined-first 3))\n```\n\n#### `alter-form: [fn-form expression]`\n\nAnaphoric macro, providing `\u0026ns`, `\u0026name`, `\u0026env-keys`, `\u0026meta`, and `\u0026form`.\n\nThis will wrap the entire form, with the form's location in the code specified by `\u0026form`. `\u0026form` must be assumed to be a *single valid expression*, not a sequence of expressions.\n\nExample:\n\n```clojure\n(alter-form fn-form\n           `(binding [*my-var* 3] \u0026form))\n```\n\n#### `prefix-bodies: [fn-form expression]`\n\nAnaphoric macro, providing `\u0026ns`, `\u0026name`, `\u0026env-keys`, `\u0026meta`, and `\u0026params`.\n\nThis will prefix each body of the function with the provided expression. `\u0026params` will evaluate to the parameter list corresponding to each body.\n\nExample:\n\n```clojure\n(prefix-bodies fn-form\n               `(assert (even? 4)\n                        (format \"Math still works in the %s arity.\"\n                                \u0026params)))\n```\n\n#### `alter-bodies: [fn-form expression]`\n\nAnaphoric macro, providing `\u0026ns`, `\u0026name`, `\u0026env-keys`, `\u0026meta`, `\u0026params`, and `\u0026body`.\n\nFor each arity of the function, this *replaces* the clauses with the given expression; `\u0026params` and `\u0026body` are bound appropriately for each arity, and `\u0026body` is assumed to be a *sequence of valid expressions*, not a single valid expression. Typically used for wrapping each body somehow.\n\nExample:\n\n```\n(alter-bodies fn-form\n             `(binding [*some-scope* ~{:ns \u0026ns,\n                                       :sym \u0026name,\n                                       :arity \u0026params}]\n                ~@\u0026body))\n```\n\n#### `*warn-on-noop*`\n\nFalse by default. When this is true, morphe will attempt to generate compile-time warnings whenever `morphe.core/defn` is unable to find any aspects to apply (perhaps due to a typo).\n\n## Examples\n\n### Logging/tracing call sites\n\nLet's say you want to log every time a method is called, along with the arity. Usually you want this to be at the warn level, but sometimes you want debug or info.\n\n```clojure\n(defn logged\n  \"Higher order fn, returning an aspect fn. Inserts a log call as the\n   first item in each fn body.\"\n  ([] (logged :warn))\n  ([level]\n   (fn [fn-form]\n     (d/prefix-bodies\n       fn-form\n       `(log/log ~level\n                 ~(format \"Logging at %s level: Entering fn %s/%s:%s.\"\n                          level\n                          \u0026ns\n                          \u0026name\n                          \u0026params))))))\n\n;; Now let's use it:\n(m/defn ^{::m/aspects [(logged :debug)]}\n        my-logged-fn\n  ([x] x)\n  ([x y] (+ x y))\n  ([x y z] (+ x y z))\n  ([x y z \u0026 more] (apply + x y z more)))\n  \n;; This expands to:\n(defn my-logged-fn\n  ([x]\n    (log/log :debug \"Logging at :debug level: Entering fn my-ns/my-logged-fn:[x].\")\n    x)\n  ([x y]\n    (log/log :debug \"Logging at :debug level: Entering fn my-ns/my-logged-fn:[x y].\")\n    (+ x y))\n  ([x y z]\n    (log/log :debug \"Logging at :debug level: Entering fn my-ns/my-logged-fn:[x y z].\")\n    (+ x y z))\n  ([x y z \u0026 more]\n    (log/log :debug \"Logging at :debug level: Entering fn my-ns/my-logged-fn:[x y z \u0026 more].\")\n    (apply + x y z more)))\n```\n\n### Tagging for metrics\n\nNow suppose you want to time a function.\n\n```clojure\n(defn timed\n  \"Creates a lexical scope for the defn with a codehale timer defined, which is\n  then used to time each function call.\"\n  [fn-form]\n  (let [timer (gensym 'timer)]\n    (-\u003e fn-form\n        (d/alter-form `(let [~timer (metrics/timer ~(format \"Timer for the function: %s\"\n                                                           (symbol (str \u0026ns) \u0026name)))]\n                        (metrics/register metrics/DEFAULT ~[(str \u0026ns) (str \u0026name) \"timer\"])\n                        ~@\u0026form))\n        (d/alter-bodies `(metrics/with-timer ~timer ~@\u0026body)))))\n\n;; Let's use it:\n(m/defn ^{::m/aspects [timed]}\n        my-timed-fn\n  ([x] x)\n  ([x y] (+ x y))\n  ([x y z] (+ x y z))\n  ([x y z \u0026 more] (apply + x y z more)))\n  \n;; This expands to:\n(let [timer7068 (metrics/timer \"Timer for the function: my-ns/my-timed-fn\")]\n  (metrics/register metrics/DEFAULT [\"my-ns\" \"my-timed-fn\" \"timer\"])\n  (defn my-timed-fn\n    ([x]\n      (metrics/with-timer timer7068\n        x))\n    ([x y]\n      (metrics/with-timer timer7068\n        (+ x y)))\n    ([x y z]\n      (metrics/with-timer timer7068\n        (+ x y z)))\n    ([x y z \u0026 more]\n      (metrics/with-timer timer7068\n        (apply + x y z more))))\n```\n\n### Mix \u0026 match\n\nLet's do both.\n\n```clojure\n(m/defn ^{::m/aspects [timed (logged :debug)]}\n        my-amazing-fn\n  ([x] x)\n  ([x y] (+ x y))\n  ([x y z] (+ x y z))\n  ([x y z \u0026 more] (apply + x y z more)))\n  \n;; This expands to:\n(let [timer7068 (metrics/timer \"Timer for the function: my-ns/my-amazing-fn\")]\n  (metrics/register metrics/DEFAULT [\"my-ns\" \"my-amazing-fn\" \"timer\"])\n  (defn my-amazing-fn\n    ([x]\n      (log/log :debug \"Logging at :debug level: Entering fn my-ns/my-amazing-fn:[x].\")\n      (metrics/with-timer timer7068\n        x))\n    ([x y]\n      (log/log :debug \"Logging at :debug level: Entering fn my-ns/my-amazing-fn:[x y].\")\n      (metrics/with-timer timer7068\n        (+ x y)))\n    ([x y z]\n      (log/log :debug \"Logging at :debug level: Entering fn my-ns/my-amazing-fn:[x y z].\")\n      (metrics/with-timer timer7068\n        (+ x y z)))\n    ([x y z \u0026 more]\n      (log/log :debug \"Logging at :debug level: Entering fn my-ns/my-amazing-fn:[x y z \u0026 more].\")\n      (metrics/with-timer timer7068\n        (apply + x y z more))))\n```\n\nChange the aspects' order in the tagged vector, change the order of application:\n\n```clojure\n(m/defn ^{::m/aspects [(logged :debug) timed]}\n        my-amazing-fn\n  ([x] x)\n  ([x y] (+ x y))\n  ([x y z] (+ x y z))\n  ([x y z \u0026 more] (apply + x y z more)))\n  \n;; This expands to:\n(let [timer7068 (metrics/timer \"Timer for the function: my-ns/my-amazing-fn\")]\n  (metrics/register metrics/DEFAULT [\"my-ns\" \"my-amazing-fn\" \"timer\"])\n  (defn my-amazing-fn\n    ([x]\n      (metrics/with-timer timer7068\n        (log/log :debug \"Logging at :debug level: Entering fn my-ns/my-amazing-fn:[x].\")\n        x))\n    ([x y]\n      (metrics/with-timer timer7068\n        (log/log :debug \"Logging at :debug level: Entering fn my-ns/my-amazing-fn:[x y].\")\n        (+ x y)))\n    ([x y z]\n      (metrics/with-timer timer7068\n        (log/log :debug \"Logging at :debug level: Entering fn my-ns/my-amazing-fn:[x y z].\")\n        (+ x y z)))\n    ([x y z \u0026 more]\n      (metrics/with-timer timer7068\n        (log/log :debug \"Logging at :debug level: Entering fn my-ns/my-amazing-fn:[x y z \u0026 more].\")\n        (apply + x y z more))))\n```\n\n### Macrotic Transformations\n\nIn the examples so far, similar effects could be achieved via (possibly clunky) functional composition. Perhaps embedding the `\u0026params` vector exactly as written in source code would be difficult, though one might get around this by exploiting metadata on vars. But the fact that we are operating on the function's *code* rather than the function itself does allow interesting transformations one could not effect purely functionally. Consider this funny little example I have used in practice (notice how some of the code gets restructured in the second arity):\n\n```clojure\n(m/defn ^{::m/aspects [(synchronize-on state #{pojo-1 pojo-2})]}\n        safely-update-then-calculate\n  ([state pojo-1]\n    (when-let [x (.inspect pojo-1)]\n      (.update pojo-1 (:one @state))\n      (expensive-calculation x))\n  ([state pojo-1 pojo-2]\n    (when-let [x (.inspect pojo-1)]\n      (.update pojo-1 (:one @state))\n      (.update pojo-2 (:two @state))\n      (expensive-calculation x))))\n\n;; expands to:\n(defn safely-update-then-calculate\n  ([state pojo-1]\n    (when-let [x (locking state\n                   (when-let [x46735 (.inspect pojo-1)]\n                     (.update pojo-1 (:one @state))\n                     x46735))]\n      (expensive-calculation x)))\n  ([state pojo-1 pojo-2]\n    (when-let [x (locking state\n                   (when-let [x46736 (.inspect pojo-1)]\n                     (.update pojo-1 (:one @state))\n                     (.update pojo-2 (:two @state))\n                     x46736))]\n       (expensive-calculation x))))\n```\n\n## Maintainers and Contributors\n\n### Active Maintainers\n\n-\n\n### Previous Contributors\n\n- Timothy Dean \u003cgaldre@gmail.com\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FWorkiva%2Fmorphe","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FWorkiva%2Fmorphe","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FWorkiva%2Fmorphe/lists"}