JSON import eased by Scripting support?

I’ve recently been making a goal-tracking system using the excellent iOS app “Drafts”, where I create a series of daily/weekly prompts about goals and outcomes, which connects with a fairly intricate JSON file (basically, an array of key/value pairs; objects which contain several nested arrays of key/value pairs themselves).

As I fill this JSON file up with data, I think it might be interesting to import into Tinderbox and mess around with it. I gather from previous threads that one straightforward way to do this is to script the JSON to output a CSV file. I think this is feasible in some ways for my data, but because of all the hierarchical nesting, I think it would probably mean a series of carefully made CSV files, which introduces a lot of opportunity for error and extra work.

I’m curious about whether anyone has used the new Scripting support to ease JSON import into Tinderbox? I’m wondering whether one might be able to write a javascript script that looped through the JSON to create hierarchically organized notes directly. (Bonus points if anyone can tell me whether the bare minimum javascript I’ve learned to manipulate JSON in Drafts will be relevant to this task as well, or if the environment is too different.)

2 Likes

Sounds very interesting!
Do you have more information about the script in Drafts? And perhaps a JSON example?

As @cremoer suggests, if you can show us a JSON sample (and explain anything about its mapping to Tinderbox object that might not be self-evident) we can sketch a use of JSON.parse() with the osascript JS API.

1 Like

Well, the Drafts scripts are still a work in progress. They are basically a series of prompts aiming at filling in or displaying information from the JSON pasted below. I have a series of goals I’ve described, and each goal has a series of priorities (only one of which is ever open, the others are all “completed”). Each priority has an array of “daily” information, including tasks I’m working on and whether I worked “enough” toward that goal that day. I’m hoping to produce some HTML displays of the information as well down the road, and I’ve worked out the technical aspects of that, just need to do some design work.

At any rate, it’s all fairly closely based on a web app called Complice, but I don’t particularly like web apps, and I wanted to shuttle the tasks around into my various iOS apps, so I thought it might be worthwhile to rebuild it in Drafts and then evolve to my uses. I’m sort of hesitant to share widely cause I think the Complice app is great and the people behind it as well and don’t want to steer people away from it.

With all that said, what I imagine for Tinderbox is something that merges the attributes in the JSON with the hierarchy as well. Like, I can imagine a Tinderbox outline like so:

Goal Name (with attributes "creation date", "complete", "completion date", "number" and $Text being the "description")
    Priority Name (with similar attributes)
        Daily Note (with $Name being some kind of $Date representation, and attributes "enough" and "description")
            Task Name (with attribute "complete" and "date")
            Task Name
        Daily Note
    Priority Name (completed priority ...)
Goal Name (2)
    Priority Name
        Daily Note
             Task
        Daily Note
etc.

But really, I’m not entirely sure! It’s an interesting information architecture exercise. I reshaped the JSON twice in the course of making the Drafts scripts.

At any rate, here’s a sample of the JSON I’m working with:

[
    {
        "goal": 1,
        "name": "Learn Javascript",
        "description": "This language has value across many different apps and platforms, it's time to learn it properly",
        "creation date": "2020-01-01",
        "complete": false,
        "completion date": "",
        "priority": [
            {
                "name": "Find the best learning resource (course, book, online tutorial)",
                "description": "",
                "creation date": "2020-01-05",
                "complete": false,
                "completion date": "",
                "daily": [
                    {
                        "date": "2020-01-08",
                        "tasks": [
                            {
                                "name": "look for reviews of online courses",
                                "complete": false
                            },
                            {
                                "name": "look for reviews of books",
                                "complete": false
                            }
                        ],
                        "enough": false,
                        "description": ""
                    },
                    {
                        "date": "2020-01-07",
                        "tasks": [],
                        "enough": true,
                        "description": "day off"
                    },
                    {
                        "date": "2020-01-06",
                        "tasks": [],
                        "enough": true,
                        "description": "day off"
                    },
                    {
                        "date": "2020-01-05",
                        "tasks": [],
                        "enough": true,
                        "description": "day off"
                    },
                    {
                        "date": "2020-01-06",
                        "tasks": [],
                        "enough": true,
                        "description": "day off"
                    },
                    {
                        "date": "2020-01-05",
                        "tasks": [],
                        "enough": true,
                        "description": "day off"
                    }
                ]
            },
            {   
                "name": "Decide on time frame for learning",
                "description": "Make a plan for learning javascript this semester",
                "creation date": "2020-01-01",
                "complete": true,
                "completion date": "2020-01-04",
                "daily": [
                    {
                        "date": "2020-01-04",
                        "tasks": [
                            {
                                "name": "sketch out weekly work schedule",
                                "complete": true
                            }
                        ],
                        "enough": true,
                        "description": "Made some thorough notes and a good plan for working on this."
                    },
                    {
                        "date": "2020-01-03",
                        "tasks": [
                            {
                                "name": "get a sense of existing time committments and how much I can invest in learning javascript",
                                "complete": true
                            }
                        ],
                        "enough": true,
                        "description": "Totaled up all the other stuff I'm doing and decided how much time I can devote to js."
                    },
                    {
                        "date": "2020-01-02",
                        "tasks": [],
                        "enough": true,
                        "description": "day off"
                    },
                    {
                        "date": "2020-01-01",
                        "tasks": [],
                        "enough": true,
                        "description": "day off"
                    }      
                ]
            }
        ]
    },
    {
        "goal": 2,
        "name": "blah blah blah",
        "description": "blah blah",
        "creation date": "2019-12-24",
        "complete": false,
        "completion date": "",
        "etc ...": ""
    }
]

Thanks for offering to help with this @ComplexPoint !

Interesting approach.
I also used Complice for some time and I really like it. Could be hard to recreate it with an app like drafts with all its bells and whistles, I guess. Especially when you see the whole thing with the reviews (weekly/monthly/yearly), chat rooms, timers, timeline, goal setting and so on…
But I am really excited to see your results.

Btw, I tried to build something similar in Tinderbox and failed :joy:
(ok… it was more playing around) …
and of course you cannot use it then on iOS, which is the next problem.

And a nice little Complice magnet on my fridge :wink:

the JSON pasted below

That looks fine – I’ll sketch something this (EU) evening.

Will it be best to assume a starting point with your JSON:

  • in a file ?
  • the clipboard ?
  • somewhere else ?
1 Like

In a file. Thank you again.

A first sketch below - will probably need some tweaking till it meets your needs.

Generally:

The trick here lies less in importing a particular piece of data from a JSON-encoded JS dictionary (easy enough) as in doing so in a way that:

  • Minimizes the writing and testing of new import code
  • Generalises enough to maximize scope for reuse next time, with other sets of data.

All of this could be done in AppleScript rather than JS, but it would risk becoming more long-winded. JS is equipped by default with JSON.parse and other useful library tools, whereas in AppleScript we have to fiddle with Foundation classes.

The difference is between, for example:

JavaScript

// jsonParseLR :: String -> Either String a
const jsonParseLR = s => {
    try {
        return Right(JSON.parse(s));
    } catch (e) {
        return Left(`${e.message} (line:${e.line} col:${e.column})`);
    }
}

AppleScript

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

-- jsonParseLR :: String -> Either String a
on jsonParseLR(s)
    set ca to current application
    set {x, e} to ca's NSJSONSerialization's ¬
        JSONObjectWithData:((ca's NSString's stringWithString:s)'s ¬
            dataUsingEncoding:(ca's NSUTF8StringEncoding)) ¬
            options:0 |error|:(reference)
    if x is missing value then
        |Left|(e's localizedDescription() as string)
    else
        if 1 = (x's isKindOfClass:(ca's NSArray)) as integer then
            |Right|(x as list)
        else
            |Right|(item 1 of (x as list))
        end if
    end if
end jsonParseLR

So the examples and full code below are all in JavaScript:

Scripted import basics

The basic manoeuvre is is to:

  • create a new note (usually with a name string)
  • and push it into the notes collection of an existing TBX note or document

e.g. (as a snippet in some broader context):

const tbx = Application('Tinderbox 8');

// Assuming that `dict` is a dictionary of Key -> Value pairs 
// in which one of the keys is probably 'Name'

// Also assuming that `parent` is either the document 
// or an existing note:

const
    newNote = tbx.Note({
        name: dict['Name'] || ''
    });
parent.notes.push(newNote);

Adding Attribute values to a note has the same pattern, in the context of a document which may or may not have an Attribute:

  • of the given name,
  • and type (drawn from the lower-case names of the set of types listed at Attributes: Data Types)

So we might see this kind of pattern (again, in the context of other code)

const maybeAttrib = attribs.byName(attributeName);
return maybeAttrib.exists() ? (
    maybeAttrib
) : (() => {
    const newAttrib = tbx.Attribute({
        name: attributeName,
        type: attributeName.endsWith('Date') ? (
            'date'
        ) : strTypeName
    });
    return (
        attribs.push(newAttrib),
        newAttrib
    );
})();
Importing hierarchically nested data

We can simplify this by:

  • writing reusable functions for importing from a very general representation of Tree-structured data,
  • and splitting our code into two stages:
    1. Converting the new data to the general Tree of Dictionaries (or Tree of Strings) structure
    2. Using existing library code to import from the general Tree structure into Tinderbox.

We can build general trees with a JS ‘constructor’ function which creates a Node.

A Node is a JS object which just links some value (a String, Dictionary, etc), to a (possibly empty) list of similar child objects.

In other words, a Tree consists of a Node which has a list of child Trees.

A Forest is a useful name for one of these lists of Trees. Most outlines for example are Forests rather than Trees – they typically have more than one top-level paragraph.

// Node :: a -> [Tree a] -> Tree a
const Node = v =>
    // Constructor for a Tree node which connects a
    // value of some kind to a list of zero or
    // more child trees.
    xs => ({
        type: 'Node',
        root: v,
        nest: xs || []
    });

Once we have read our data into this kind of Tree format, we can work on it with general functions. For example:

  • fmapTree(f)(tree) applies the same function f to every node in the given tree.
  • foldTree(f)(tree) derives a single value from a whole tree - depending on the particular function f, it might return things like:
    • The total number of items in a tree
    • the maximum depth of a tree
    • a full list of all the Attribute name keys occurring anywhere in a Tree of Dictionaries.
Mapping data keys to TBX Attribute names and types

Data from an external source may need some pre-processing to get the desired mapping to TBX Attribute names and types.

Preparing a Forest of data for import to Tinderbox can be usefully divided into two stages:

  1. Deriving the Tree (probably Forest) structure from the structure of the source data,
  2. Mapping source data keys and types to TBX Attribute names and types.

A tentative first sketch of doing this for Derek’s data above might look something like the code below.

With this particular data we are:

  • mapping the description key in the imported JSON to the Tinderbox Text attribute.
  • initially mapping the source data’s date key to a custom GoalDate attribute. (Derek may well want to change that name)
  • Adapting other keys to Camel-cased names without spaces
  • Ignoring source data keys which have empty string values
  • Making sure that every note has a Name, borrowing either the name of the key which contains the sub-tree (list of child dictionaries), or the same string as the Text attribute (itself taken from the source data description)
// MAPPINGS FROM INPUT DICTIONARIES TO TBX ATTRIBUTES
// Dictionary keys -> TBX Attribute names

// tbxKeyValues :: Dict -> Dict
const tbxKeyValues = dict =>
    // Mappings of dictionary keys and values
    // to TBX attribute names and value types.
    Object.keys(dict)
    .reduce(
        (a, k) => {
            const
                k1 = k !== 'description' ? (
                    k !== 'date' ? (
                        concat(
                            map(toSentence)(
                                words(k)
                            )
                        )
                    ) : 'GoalDate'
                ) : 'Text',
                v = dict[k],
                dctTBX = Boolean(v) ? (
                    Object.assign(
                        a, {
                            [k1]: v
                        },
                    )
                ) : a;
            return Boolean(dctTBX.Name) ? (
                dctTBX
            ) : Object.assign(
                dctTBX, {
                    Name: (
                        dctTBX['ListTitle'] ||
                        dctTBX['Text']
                    )
                }
            );
        }, {}
    );

Assuming a copy of @derekvan’s data at ~/Desktop/sample.json

First rough draft of full source
(() => {
    'use strict';

    // Importing from a sample.json file to Tinderbox
    // Rob Trew 2020
    // Draft 0.05

    // Added a couple of function to the library
    // (for reporting any ambiguous JSON nesting)

    // main :: IO ()
    const main = () => {
        const fp = '~/Desktop/sample.json';
        return either(
            alert('Unexpected data')
        )(
            alert('Imported to Tinderbox')
        )(bindLR(
            doesFileExist(fp) ? (
                Right(readFile(fp))
            ) : Left('File not found at path: ' + fp)
        )(
            strJSON => bindLR(jsonParseLR(strJSON))(
                xs => bindLR(traverseList(treeFromDict)(xs))(
                    forest => bindLR(tbxFrontDocLR())(
                        compose(
                            // Indented outline log.
                            Right, unlines,
                            indentedLinesFromForest('    ')(
                                append('- ') // Bullet prefix
                            ),
                            // Notes inserted in Tinderbox 8.
                            tbxNotesFromForest(
                                forest.map(
                                    fmapTree(tbxKeyValues)
                                )
                            )
                        )
                    )
                )
            )
        ));
    };

    // MAPPINGS FROM INPUT DICTIONARIES TO TBX ATTRIBUTES
    // Dictionary keys -> TBX Attribute names

    // tbxKeyValues :: Dict -> Dict
    const tbxKeyValues = dict =>
        // Mappings of dictionary keys and values
        // to TBX attribute names and value types.
        Object.keys(dict)
        .reduce(
            (a, k) => {
                const
                    v = dict[k],
                    dctTBX = Boolean(v) ? (
                        Object.assign(
                            a, {
                                [k !== 'description' ? (
                                    k !== 'date' ? (
                                        concat(
                                            words(k)
                                            .map(toSentence)
                                        )
                                    ) : 'GoalDate'
                                ) : 'Text']: v
                            },
                        )
                    ) : a;
                return Boolean(dctTBX.Name) ? (
                    dctTBX
                ) : Object.assign(
                    dctTBX, {
                        Name: (
                            dctTBX['ListTitle'] ||
                            dctTBX['Text']
                        )
                    }
                );
            }, {}
        );


    // TINDERBOX GENERAL ----------------------------------

    // tbxNotesFromForest ::
    //      [Tree Dict] -> TBX Document -> [Tree Note]
    const tbxNotesFromForest = forest => doc => {
        const attribs = doc.attributes;
        const go = parent => tree => {
            const
                dict = tree.root,
                newNote = tbx.Note({
                    name: dict['Name'] || ''
                });
            return (
                parent.notes.push(newNote),
                Object.keys(dict).reduce(
                    (a, k) => 'Name' !== k ? (
                        a.byName(k).value = dict[k],
                        a
                    ) : a,
                    newNote.attributes
                ),
                Node(newNote.name())(
                    tree.nest.map(go(newNote))
                )
            );
        };
        const
            tbx = Application('Tinderbox 8'),
            dctAllKeys = foldTree(
                t => ts => Object.assign({}, t, ...ts)
            )(Node({})(forest));
        return (
            // Full set of the attributes 
            // for the Key-Value Tree.
            Object.keys(dctAllKeys)
            .map(
                k => tbxAttribFoundOrCreated(tbx)(
                    attribs
                )(toLower(typeof dctAllKeys[k]))(k)
            ),
            forest.map(go(doc))
        );
    };

    // tbxAttribFoundOrCreated ::
    //      TBX App -> TBX Attributes ->
    //      String -> String -> TBX Attribute
    const tbxAttribFoundOrCreated = tbx =>
        // Either a reference to an existing attribute, 
        // if found, or to a new attribute of the given name 
        // and type, where type is a string drawn from:
        // {boolean, color, date, file, interval,
        //  list, number, set, string, url}
        attribs => strTypeName => attributeName => {
            const maybeAttrib = attribs.byName(
                attributeName
            );
            return maybeAttrib.exists() ? (
                maybeAttrib
            ) : (() => {
                const newAttrib = tbx.Attribute({
                    name: attributeName,
                    type: attributeName.endsWith('Date') ? (
                        'date'
                    ) : strTypeName
                });
                return (
                    attribs.push(newAttrib),
                    newAttrib
                );
            })();
        };

    // tbxFrontDocLR :: Tbx IO () -> Either String Tbx Doc
    const tbxFrontDocLR = () => {
        // Either the front document in Tinderbox 8, or an
        // explanatory message if no documents are open,
        // or Tinderbox 8 is not running.
        const tbx = Application('Tinderbox 8');
        return tbx.running() ? (() => {
            const ds = tbx.documents;
            return 0 < ds.length ? (
                Right(ds.at(0))
            ) : Left('No documents open in Tinderbox');
        })() : Left('Tinderbox 8 is not running.');
    };

    // JXA -------------------------------------------------

    // alert :: String -> String -> IO String
    const alert = title => s => {
        const
            sa = Object.assign(
                Application('System Events'), {
                    includeStandardAdditions: true
                }
            );
        return (
            sa.activate(),
            sa.displayDialog(s, {
                withTitle: title,
                buttons: ['OK'],
                defaultButton: 'OK'
            }),
            s
        );
    };

    // ----------------------GENERIC-----------------------

    // Node :: a -> [Tree a] -> Tree a
    const Node = v =>
        // Constructor for a Tree node which connects a
        // value of some kind to a list of zero or
        // more child trees.
        xs => ({
            type: 'Node',
            root: v,
            nest: xs || []
        });

    // Left :: a -> Either a b
    const Left = x => ({
        type: 'Either',
        Left: x
    });

    // Right :: b -> Either a b
    const Right = x => ({
        type: 'Either',
        Right: x
    });

    // append (++) :: [a] -> [a] -> [a]
    // append (++) :: String -> String -> String
    const append = xs =>
        // A list or string composed by
        // the concatenation of two others.
        ys => xs.concat(ys);

    // bindLR (>>=) :: Either a ->
    // (a -> Either b) -> Either b
    const bindLR = m =>
        mf => undefined !== m.Left ? (
            m
        ) : mf(m.Right);

    // bulleted :: String -> String -> String
    const bulleted = strTab =>
        s => s.split(/[\r\n]/).map(
            x => '' !== x ? strTab + '- ' + x : x
        ).join('\n');

    // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
    const compose = (...fs) =>
        x => fs.reduceRight((a, f) => f(a), x);

    // concat :: [[a]] -> [a]
    // concat :: [String] -> String
    const concat = xs =>
        0 < xs.length ? (
            xs.every(x => 'string' === typeof x) ? (
                ''
            ) : []
        ).concat(...xs) : xs;

    // cons :: a -> [a] -> [a]
    const cons = x =>
        xs => Array.isArray(xs) ? (
            [x].concat(xs)
        ) : 'GeneratorFunction' !== xs.constructor.constructor.name ? (
            x + xs
        ) : ( // Existing generator wrapped with one additional element
            function* () {
                yield x;
                let nxt = xs.next()
                while (!nxt.done) {
                    yield nxt.value;
                    nxt = xs.next();
                }
            }
        )();

    // deleteKey :: String -> Dict -> Dict
    const deleteKey = k =>
        // A new dictionary, without the key k.
        dct => {
            const dct2 = {
                ...dct
            };
            return (delete dct2[k], dct2);
        };

    // doesFileExist :: FilePath -> IO Bool
    const doesFileExist = fp => {
        const ref = Ref();
        return $.NSFileManager.defaultManager
            .fileExistsAtPathIsDirectory(
                $(fp)
                .stringByStandardizingPath, ref
            ) && 1 !== ref[0];
    };

    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl =>
        fr => e => 'Either' === e.type ? (
            undefined !== e.Left ? (
                fl(e.Left)
            ) : fr(e.Right)
        ) : undefined;

    // elem :: Eq a => a -> [a] -> Bool
    // elem :: Char -> String -> Bool
    const elem = x =>
        xs => xs.includes(x);

    // foldTree :: (a -> [b] -> b) -> Tree a -> b
    const foldTree = f => tree => {
        const go = node => f(node.root)(
            node.nest.map(go)
        );
        return go(tree);
    };

    // fmapTree :: (a -> b) -> Tree a -> Tree b
    const fmapTree = f => tree => {
        const go = node => Node(f(node.root))(
            node.nest.map(go)
        );
        return go(tree);
    };

    // fst :: (a, b) -> a
    const fst = tpl => tpl[0];

    // identity :: a -> a
    const identity = x =>
        // The identity function.
        // (The argument unchanged)
        x;

    // indentedLinesFromForest :: String -> (a -> String) ->
    //      [Tree a] -> [String]
    const indentedLinesFromForest = strTab =>
        // Indented text representation of a list of Trees.
        // f is an (a -> String) function defining
        // the string representation of tree nodes.
        f => trees => {
            const go = indent => node => [indent + f(node.root)]
                .concat(node.nest.flatMap(go(strTab + indent)));
            return trees.flatMap(go(''));
        };

    // jsonParseLR :: String -> Either String a
    const jsonParseLR = s => {
        try {
            return Right(JSON.parse(s));
        } catch (e) {
            return Left(`${e.message} (line:${e.line} col:${e.column})`);
        }
    };

    // Lift a binary function to actions.
    // liftA2 f a b = fmap f a <*> b

    // liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
    const liftA2 = f => a => b => {
        const t = typeName(a);
        return (
            'Bottom' !== t ? (
                '(a -> b)' === t ? (
                    liftA2Fn
                ) : 'Either' === t ? (
                    liftA2LR
                ) : 'Maybe' === t ? (
                    liftA2May
                ) : 'Tuple' === t ? (
                    liftA2Tuple
                ) : 'Node' === t ? (
                    liftA2Tree
                ) : liftA2List
            ) : liftA2List
        )(f)(a)(b);
    };

    // liftA2LR :: (a -> b -> c) -> Either d a -> Either d b -> Either d c
    const liftA2LR = f => a => b =>
        undefined !== a.Left ? (
            a
        ) : undefined !== b.Left ? (
            b
        ) : Right(f(a.Right)(b.Right))

    // pureLR :: a -> Either e a
    const pureLR = x => Right(x);

    // Given a type name string, returns a
    // specialised 'pure', where
    // 'pure' lifts a value into a particular functor.

    // pureT :: String -> f a -> (a -> f a)
    const pureT = t => x =>
        'List' !== t ? (
            'Either' === t ? (
                pureLR(x)
            ) : 'Maybe' === t ? (
                pureMay(x)
            ) : 'Node' === t ? (
                pureTree(x)
            ) : 'Tuple' === t ? (
                pureTuple(x)
            ) : pureList(x)
        ) : pureList(x);

    // readFile :: FilePath -> IO String
    const readFile = fp => {
        const
            e = $(),
            ns = $.NSString.stringWithContentsOfFileEncodingError(
                $(fp).stringByStandardizingPath,
                $.NSUTF8StringEncoding,
                e
            );
        return ObjC.unwrap(
            ns.isNil() ? (
                e.localizedDescription
            ) : ns
        );
    };


    // showJSON :: a -> String
    const showJSON = x => JSON.stringify(x, null, 2);

    // showLog :: a -> IO ()
    const showLog = (...args) =>
        console.log(
            args
            .map(JSON.stringify)
            .join(' -> ')
        );

    // toSentence :: String -> String
    const toSentence = s =>
        // Sentence case - initial string capitalized
        // and rest lowercase.
        (0 < s.length) ? (
            s[0].toUpperCase() + s.slice(1)
            .toLowerCase()
        ) : s;

    // toLower :: String -> String
    const toLower = s => s.toLocaleLowerCase();

    // traverseList :: (Applicative f) => (a -> f b) -> [a] -> f [b]
    const traverseList = f =>
        // Collected results of mapping each element
        // of a structure to an action, and evaluating
        // these actions from left to right.
        xs => {
            const lng = xs.length;
            return 0 < lng ? (() => {
                const
                    vLast = f(xs[lng - 1]),
                    t = vLast.type || 'List';
                return xs.slice(0, -1).reduceRight(
                    (ys, x) => liftA2(cons)(f(x))(ys),
                    liftA2(cons)(vLast)(pureT(t)([]))
                );
            })() : [
                []
            ];
        };

    // treeFromDict -> Dict -> Either String Tree Dict
    const treeFromDict = dict => {
        // A generic Tree structure from a dict
        // with keys assumed to include no more than
        // one key to a *list* value,
        // with this pattern applied recursively
        // to each child dictionary in such a list.
        const go = dct => {
            const
                kvs = Object.entries(dct),
                lists = kvs.filter(
                    ([_, v]) => Array.isArray(v)
                ),
                lng = lists.length;
            return 0 < lng ? (
                1 < lng ? (
                    Left(
                        'Ambiguous structure :: ' +
                        lng.toString() + (
                            ' multiple sublists in:\n  "' +
                            dct.name + (
                                '":\n' + bulleted('    ')(
                                    unlines(lists.map(fst))
                                )
                            )
                        )
                    )
                ) : (() => {
                    const [nestName, xs] = lists[0];
                    return bindLR(traverseList(go)(xs))(
                        xs => Right(
                            Node(
                                Object.assign(
                                    deleteKey(nestName)(
                                        dct
                                    ), {
                                        'List title': nestName
                                    }
                                )
                            )(xs)
                        )
                    );
                })()
            ) : Right(Node(dct)([]))
        };
        return go(dict);
    };

    // typeName :: a -> String
    const typeName = v => {
        const t = typeof v;
        return 'object' === t ? (
            Array.isArray(v) ? (
                'List'
            ) : null !== v ? (
                v.type || 'Dict'
            ) : 'Bottom'
        ) : {
            'boolean': 'Bool',
            'number': 'Num',
            'string': 'String',
            'function': '(a -> b)'
        } [t] || 'Bottom';
    };

    // unlines :: [String] -> String
    const unlines = xs => xs.join('\n');

    // words :: String -> [String]
    const words = s => s.split(/\s+/);

    return main();
})();
2 Likes

Just noticed a regression there – we’re getting too many day offs in lieu of .Names in Draft 0.01 :slight_smile:

Cooking here now, but I’ll look at that (if you haven’t already fixed it), tomorrow evening.

UPDATE

Fixed in listing above (now ver 0.02).

As with almost all bugs, this one was caused by mutable data.

(Hence the predominant use of const above, in lieu, wherever possible of let and the even messier var – see Eloquent JavaScript – the first 10 chapters are all relevant to scripting, but I personally use an even smaller subset of JS)

It was fixed by rewriting line 125 (collection of full list of import data keys) from:

t => ts => Object.assign(t, ...ts)

to

t => ts => Object.assign({}, t, ...ts)

Object.assign is very useful built-in method which lets us merge two or more dictionaries into one,

  • combining all their key-value pairs,
  • and in case of clashes, giving priority to dictionaries further right in its argument list.

In haste and hubris, when I first typed that expression, I neglected to include a leftmost {}, a fresh and empty dictionary in which to accumulate key values from all the other dictionaries and sub-dictionaries on @derekvan’s data.

With that {}, we are working with immutable values for all the names in our code. t and ts remain unchanged while we collect their contents in the new dictionary.

Without the {}, we are mutating the meaning of t, overwriting some of its key-value pairs with material from ts.

Donald Knuth’s formulation is that Premature optimization is the root of all evil.

An energetic and highly-qualified competitor would be the use of mutable or variable values for names in our scripts :slight_smile:

Whenever you reach for Object.assign(), start by writing Object.assign({}, ) as a reflex.

(I’m going to put an ,,oa expansion for Object.assign({}, {^}) into Typinator)

Wow, this looks great. I think it will take me some days to work through this and try to understand it. Functions are just above my level of understanding, but I do have a broad sense of how they work so I think I can muddle through it, especially with your excellent overview of how the whole process works.

Thanks again, as always, for your generosity here Rob!

Perhaps the key insight is to realise that function is a value with a gap.

Once the gap is filled with a specific value, the function ‘reduces’ to a ‘fully saturated’ value.

For example, if we define a function double as below:

    // double :: Int -> Int
    const double = x => 2 * x;

Then we know that double is an integer value with an integer gap which we call x.

Once we fill x with a specific integer, then we will get a simple, fully saturated and particular integer value.

The expression double(4) (a ‘function application’, or gap-filling) reduces to the simple gapless value 8.

(() => {
    'use strict';

    // double :: Int -> Int
    const double = x => 2 * x;

    console.log(
        double(4)
    );
    // --> 8
})();

Fix in Ver 0.02+ (above)

With a file containing @derekvan’s JSON at ~/Desktop/sample.json,

we should now get:

  • a simple report dialog,
  • and a nested import to Tinderbox 8.

1 Like

So great! This is super cool. Can’t wait to spend some time unpacking this. Hope other people are able to reuse this code for their own JSON work.

1 Like

The only code in there that is specific to your data is the particular set of key → attribute mappings in lines 46-83 (in other words the particular contents of the tbxKeyValues function).

The rest is generic – usable with any JSON list of nested key-value dictionaries.

2 Likes