{"id":28566826,"url":"https://github.com/salsify/botanist","last_synced_at":"2025-10-28T22:31:28.556Z","repository":{"id":6885539,"uuid":"55198392","full_name":"salsify/botanist","owner":"salsify","description":"A JavaScript DSL for traversing and transforming data based on structural rules","archived":false,"fork":false,"pushed_at":"2024-02-07T15:10:47.000Z","size":90,"stargazers_count":28,"open_issues_count":1,"forks_count":1,"subscribers_count":16,"default_branch":"master","last_synced_at":"2025-05-16T02:08:06.476Z","etag":null,"topics":["npm"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","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/salsify.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":"2016-04-01T02:42:16.000Z","updated_at":"2024-08-07T00:45:10.000Z","dependencies_parsed_at":"2024-06-19T13:22:58.533Z","dependency_job_id":"1ce4a0f6-0aa9-48b2-aee0-e0145368531e","html_url":"https://github.com/salsify/botanist","commit_stats":{"total_commits":25,"total_committers":4,"mean_commits":6.25,"dds":0.12,"last_synced_commit":"a6f5345d5b56b4ebf54393b898eceab9e82f0cfd"},"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/salsify%2Fbotanist","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/salsify%2Fbotanist/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/salsify%2Fbotanist/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/salsify%2Fbotanist/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/salsify","download_url":"https://codeload.github.com/salsify/botanist/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/salsify%2Fbotanist/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259104095,"owners_count":22805808,"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":["npm"],"created_at":"2025-06-10T15:39:14.675Z","updated_at":"2025-10-28T22:31:28.492Z","avatar_url":"https://github.com/salsify.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Botanist [![Build Status](https://travis-ci.org/salsify/botanist.svg?branch=master)](https://travis-ci.org/salsify/botanist)\n\nA JavaScript DSL for taming tree structures using rules about the parts they're composed from. Inspired by Parslet's [Transforms](http://kschiess.github.io/parslet/transform.html), Botanist allows you to define a transformation over arbitrarily complex data by describing the structure of the specific constituents that you're interested in.\n\n## Getting Started\n\nA Botanist **transform** is composed of one or more **rules**. Each rule declares the structure it intends to match and a function to be called any time a matching structure is found. Note that this sequence of examples uses the proposed [decorator syntax](https://github.com/wycats/javascript-decorators) for declaring rules, but if you'd prefer plain old ES5, examples of how to do that are [further below](#usage-without-decorators).\n\n### Hello, World\n\nLet's start with nearly the simplest possible rule. We'd like to match any object with a single key `message` whose value is `'hello'`. If we find such an object, we'd like to expand the scope of that message to address the entire world.\n\n```js\nimport { transform, rule } from 'botanist';\n\nlet myFirstTransform = transform({\n  @rule({ message: 'hello' })\n  expandHorizons() {\n    return { message: 'hello', scope: 'world' }};\n  }\n});\n\nmyFirstTransform({ message: 'hello' });\n// =\u003e { message: 'hello', scope: 'world' }\n\nmyFirstTransform({});\n// =\u003e {}\n\nmyFirstTransform({ message: 'hello', irrelevant: true });\n// =\u003e { message: 'hello', irrelevant: true }\n\nmyFirstTransform({ deeply: { nested: { message: 'hello' } } });\n// =\u003e { deeply: { nested: { message: 'hello', scope: 'world' } } }\n```\n\nSome important things to keep in mind about rules:\n - `expandHorizons()` could have been called anything, but picking a method name that describes what the rule does can be helpful for readability and testing\n - anything that doesn't match a rule will come out the other side untouched\n - an object-based rule will only apply to an object with exactly the same keys as the rule itself\n - rules can also match based on the contents of arrays (e.g. `@rule([1, 2, 3])`)\n - a single rule can apply to multiple different substructures in one transformation\n\n### Capturing Data\n\nWhile there are plenty of scenarios where we may know the exact structure we'd like to match, what about cases where we only know part of it? For this, Botanist supplies a set of **matchers** which can match and capture data in positions where you don't necessarily know what's going to appear. The simplest matcher is (shockingly) called `simple`. It will match any JavaScript primitive like a number or string, and pass it through to the rule's function using the given name.\n\n```js\nimport { transform, rule, simple } from 'botanist';\n\nlet doMath = transform({\n  @rule({ op: 'add', lhs: simple('left'), rhs: simple('right') })\n  add({ left, right }) {\n    return left + right;\n  },\n\n  @rule({ op: 'sub', lhs: simple('left'), rhs: simple('right') })\n  subtract({ left, right }) {\n    return left - right;\n  }\n});\n\ndoMath({ op: 'add', lhs: 1, rhs: 2 });\n// =\u003e 3\n\ndoMath({ op: 'sub', lhs: { op: 'add', lhs: 2, rhs: 2 }, rhs: 1 });\n// =\u003e 3\n\ndoMath({ op: 'add', lhs: [1, 2], rhs: 3 });\n// =\u003e { op: 'add', lhs: [1, 2], rhs: 3 }\n```\n\nNote that you may bind two different fields in a single rule to the same name. If you do so, the rule will only be considered to match if both fields have the same value (according to `===`).\n\nMore information about `simple` and the other available matchers can be found in a [dedicated section](#available-matchers) below.\n\n### Relaxing Structural Restrictions\n\nBy default, `@rule({ tag: 'important' })` will only match objects whose _only_ key is `tag` with the value `'important'`. What if we want to match any object tagged as important, regardless of the rest of its structure? Enter `rest()`.\n\n```js\nimport { transform, rule, rest } from 'botanist';\n\nlet emphasizeImportantThings = transform({\n  @rule({ tag: 'important', ...rest('item') })\n  emphasizeIt({ item }) {\n    let result = {};\n    for (let [key, value] of Object.entries(item)) {\n      result[key.toUpperCase()] = `${value}`.toUpperCase();\n    }\n    return result;\n  }\n});\n\nemphasizeImportantThings([\n  { tag: 'important', message: 'uh oh' },\n  { tag: 'snoozed', key: 'nbd' },\n  { tag: 'important', subject: 'hi', content: 'are you there?' }\n]);\n// =\u003e [{ MESSAGE: 'UH OH' }, { tag: 'snoozed', key: 'nbd' }, { SUBJECT: 'HI', CONTENT: 'ARE YOU THERE?' }]\n```\n\nYou can also use `rest` to bind the remaining elements of an array:\n\n```js\nimport { transform, rule, simple, rest } from 'botanist';\n\nlet queenOfHearts = transform({\n  @rule([simple('head'), ...rest('tail')])\n  offWithIt({ head, tail }) {\n    return { head, tail };\n  }\n});\n\nqueenOfHearts([1, 2, 3, 4, 5]);\n// =\u003e { head: 1, tail: [2, 3, 4, 5] }\n```\n\nIf you're operating in an environment without the `...` spread operator, `rest` also has an ES5-compatible usage pattern where it wraps the pattern in question instead of spreading into it.\n\nFor objects, rather than writing `{ x: 'y', ...rest('remainder') }`, you'd write `rest({ x: 'y' }, 'remainder')`.\n\nFor arrays, `[1, 2, ...rest('remainder')]` becomes `rest([1, 2], 'remainder')`.\n\n### Rule Interactions\n\nRules are applied in the order given, so if two rules could both match the same object, the first one will \"win\" and be applied. Rules are also applied from the bottom up, so all properties of an object will be considered and potentially transformed before the object itself is evaluated.\n\n```js\nimport { transform, rule, simple, sequence } from 'botanist';\n\nlet makeValueJudgments = transform({\n  @rule([simple('first'), simple('second'), simple('third')])\n  shoutItOut({ first, second, third }) {\n    return [first, second, third].map(item =\u003e item.toUpperCase());\n  },\n\n  @rule({ value: 42 })\n  handleSpecialValue() {\n    return 'special';\n  },\n\n  @rule({ value: simple('value') })\n  handleBoringValue({ value }) {\n    return `ordinary (${value})`;\n  }\n});\n\nmakeValueJudgments([\n  { value: 1 },\n  { value: 42 },\n  { value: 100 }\n]);\n// =\u003e ['ORDINARY (1)', 'SPECIAL', 'ORDINARY (100)']\n```\n\n## Available Matchers\n### `simple(name)`\nThe `simple` matcher will bind any JS primitive or custom class instance to the given name. It may seem strange that custom classes are considered \"simple\", but since Botanist only traverses down through arrays and POJOs by design, instances of `MyClass` are treated as terminal \"leaf\" nodes.\n\nMany use cases for Botanist involve taking a JSON-compatible object and either distilling it down to a simpler encoding such as a string, or building it into a richer one, such as a hierarchy of complex objects with their own methods and prototype chains. Because of this, matching on `simple` subtrees is a way to help catch bugs in your transform early, since a deep JSON object that no rule matches will ripple its \"non simpleness\" back up to the top, preventing any of its ancestors from matching as well and making it clear where the problem was introduced.\n\n#### Sample Values `simple` Will Match\n- `null`\n- `undefined`\n- `false`\n- `87`\n- `'hello'`\n- `new MyClass()`\n\n#### Sample Values `simple` Will Not Match\n- `[1, 2, 3]`\n- `{ x: 1, y: 2 }`\n- `[]`\n- `{}`\n\n### `choice(options, name)`\nThe `choice` matcher will bind any `simple` value that is present in a given list of options.\n\n#### Sample Values `choice` Will Match\n- `choice([1, 2, 3])`\n  - `1`\n  - `2`\n  - `3`\n- `choice(['hello'])`\n  - `'hello'`\n\n#### Sample Values `choice` Will Not Match\n- `choice([1, 2, 3])`\n  - `4`\n  - `'1'`\n- `choice([null, false])`\n  - `undefined`\n  - `''`\n  - `[]`\n- `choice([])`\n  - `null`\n  - `undefined`\n  - `''`\n  - `[]`\n\n### `sequence(name)`\nThe `sequence` matcher will bind an array of `simple` elements to the given name.\n\n#### Sample Values `sequence` Will Match\n- `[1, 2, 3]`\n- `['hi', false, new MyClass()]`\n- `[]`\n\n#### Sample Values `sequence` Will Not Match\n- `[{}]`\n- `[[1, 2, 3]]`\n- `{}`\n- `'sequence'`\n\n### `match(regex, name)`\nThe `match` matcher accepts a regular expression, and will bind an array of captured strings to the given name. The final element of the array will be the full matched string.\n\n#### Sample Values `match` Will Match\n- `match(/foo(bar|baz)/)`\n  - `'foobar'` =\u003e `['bar', 'foobar']`\n  - `'foobaz'` =\u003e `['baz', 'foobaz']`\n  - `'abcfoobardef'` =\u003e `['bar', 'foobar']`\n- `match(/\\d+/)`\n  - `'123'` =\u003e `['123']`\n  - `123` =\u003e `['123']`\n- `match(/.*/)`\n  - `undefined` =\u003e `['undefined']`\n  - `null` =\u003e `['null']`\n  - `true` =\u003e `['true']`\n  - `new MyClass()` =\u003e `['[object Object]']`\n\nNote that `match` will coerce any `simple` value to a string before evaluating whether or not it matches the given regular expression.\n\n#### Sample Values `match` Will Not Match\n- `match(/foo(bar|baz)/)`\n  - `''`\n  - `'qux'`\n- `match(/.*/)`\n  - `[]`\n  - `{}`\n\n### `subtree(name)`\nThe `subtree` matcher binds any value to the given name, including arrays and POJOs. Use with caution, as this escape valve has the potential to be a footgun. If you know anything about the structure you're attempting to match, you're probably better off using `rest`, but if you truly want to match anything, `subtree` is an efficient way to do it.\n\n## Transform Options\nWhen running a transform, you can pass a second argument if you want to be able to customize its behavior. This value will be exposed to every rule function as it executes.\n\n```js\nlet replaceNames = transform({\n  @rule({ name: simple('name') })\n  replaceName({ name }, replacements = {}) {\n    return { name: replacements[name] || name };\n  }\n});\n\nlet people = [\n  { name: 'Alice' },\n  { name: 'Bob' }\n];\n\nreplaceNames(people);\n// =\u003e [{ name: 'Alice' }, { name: 'Bob' }]\n\nreplaceNames(people, { Bob: 'Barbara' });\n// =\u003e [{ name: 'Alice' }, { name: 'Barbara' }]\n\nreplaceNames(people, { Alice: 'Alex', Bob: 'Brad' });\n// =\u003e [{ name: 'Alex' }, { name: 'Brad' }]\n```\n\n## Modularity and Composition\n\nRather than a single object with rules, `transform` will also accept an array of such objects. In this way, you have the option of packaging up your rules into smaller logical groups that you can develop and test individually.\n\nThen, you can compose all those sets of rules together to produce your final transformation function. Note that, just like with a single object, rules will be tested in the order given.\n\n## Usage Without Decorators\n\nIf you wish to use Botanist in an ES5 environment (or you just don't like having to come up with names for your rules), everything documented above will also work if you treat `rule` as a regular function and just pass a second argument representing the rule's behavior.\n\nRevisiting our math example from earlier:\n\n```js\nvar botanist = require('botanist');\nvar simple = botanist.simple;\nvar rule = botanist.rule;\n\nvar doMath = botanist.transform([\n  rule({ op: 'add', lhs: simple('left'), rhs: simple('right') }, function(values) {\n    return values.left + values.right;\n  }),\n\n  rule({ op: 'sub', lhs: simple('left'), rhs: simple('right') }, function(values) {\n    return values.left - values.right;\n  })\n]);\n\ndoMath({ op: 'add', lhs: 1, rhs: 2 });\n// =\u003e 3\n\ndoMath({ op: 'sub', lhs: { op: 'add', lhs: 2, rhs: 2 }, rhs: 1 });\n// =\u003e 3\n\ndoMath({ op: 'add', lhs: [1, 2], rhs: 3 });\n// =\u003e { op: 'add', lhs: [1, 2], rhs: 3 }\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsalsify%2Fbotanist","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsalsify%2Fbotanist","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsalsify%2Fbotanist/lists"}