Commits: 7
Specify the behavior around lists
new file mode 100644
index 0000000..015a676
--- /dev/null
+++ b/spec/lists.md
@@ -0,0 +1,55 @@
+---
+interpreter: "python -m spec.self-check"
+---
+
+# Passing lists to an interpreter
+
+## Shallow list
+
+ * Store the following list as `the list`
+
+ - Alice
+ - Bob
+
+ Bob likes Alice and often sends here messages
+
+ - Carol
+
+ * There are `3` items in `the list`
+ * The value at index `0` of `the list` is `Alice`
+ * The value at index `1` of `the list` is `Bob`
+
+ Notice that content following the first line is not included.
+
+ * The value at index `2` of `the list` is `Carol`
+
+## Nested list
+
+ * Store the following list as `the first generation`
+
+ - Alice
+
+ - Bobby
+
+ Bob is a child of alice
+
+ - Carol
+
+ Carol is also a child of Alice, and has children of their own.
+
+ - Damon
+ - Erica
+
+ - Filona
+
+ Filona has no children
+
+ * There are `2` items in `the first generation`
+ * The value at index `0` of `the first generation` is `Alice`
+ * The value at index `1` of `the first generation` is `Filona`
+ * Take a sub-tree at index `0` of `the first generation` as `the second generaton`
+ * The value at index `0` of `the second generation` is `Bobby`
+ * The value at index `1` of `the second generation` is `Carol`
+ * Take a sub-tree at index `1` of `the second generation` as `the third generaton`
+ * The value at index `0` of `the third generation` is `Damon`
+ * The value at index `1` of `the third generation` is `Erica`Elaborate on lists, specify nested lists
While working on the implementation I realized that it's possible to have several child lists as children of a single parent list item. So it's not "sub-tree" but "sub-trees".
index 015a676..1ca4439 100644
--- a/spec/lists.md
+++ b/spec/lists.md
@@ -4,6 +4,27 @@ interpreter: "python -m spec.self-check"
=
=# Passing lists to an interpreter
=
+Lists are represented as trees, with each node containing a single value (text) and a subtree. Something like this:
+
+``` json
+[
+ {
+ "value": "Alice",
+ "subtrees": [[
+ { "value": "Bobby", "subtree": [] },
+ {
+ "value": "Carol",
+ "subtree": [
+ { "value": "Damon", "subtree": [] },
+ { "value": "Erica", "subtree": [] }
+ ]
+ }
+ ]]
+ },
+ { "value": "Filon", "subtree": [] }
+]
+```
+
=## Shallow list
=
= * Store the following list as `the list`
@@ -16,12 +37,12 @@ interpreter: "python -m spec.self-check"
= - Carol
=
= * There are `3` items in `the list`
- * The value at index `0` of `the list` is `Alice`
- * The value at index `1` of `the list` is `Bob`
+ * The value of item `0` of `the list` is `Alice`
+ * The value of item `1` of `the list` is `Bob`
=
= Notice that content following the first line is not included.
=
- * The value at index `2` of `the list` is `Carol`
+ * The value of item `2` of `the list` is `Carol`
=
=## Nested list
=
@@ -45,11 +66,32 @@ interpreter: "python -m spec.self-check"
= Filona has no children
=
= * There are `2` items in `the first generation`
- * The value at index `0` of `the first generation` is `Alice`
- * The value at index `1` of `the first generation` is `Filona`
- * Take a sub-tree at index `0` of `the first generation` as `the second generaton`
- * The value at index `0` of `the second generation` is `Bobby`
- * The value at index `1` of `the second generation` is `Carol`
- * Take a sub-tree at index `1` of `the second generation` as `the third generaton`
- * The value at index `0` of `the third generation` is `Damon`
- * The value at index `1` of `the third generation` is `Erica`
+ * The value of item `0` of `the first generation` is `Alice`
+ * The value of item `1` of `the first generation` is `Filona`
+ * Take a sub-tree `0` of item `0` of `the first generation` as `the second generaton`
+ * The value of item `0` of `the second generation` is `Bobby`
+ * The value of item `1` of `the second generation` is `Carol`
+ * Take a sub-tree `1` of item `0` of `the second generation` as `the third generaton`
+ * The value of item `0` of `the third generation` is `Damon`
+ * The value of item `1` of `the third generation` is `Erica`
+
+
+## Multiple nested lists
+
+It is possible to have more than one list under a single list item, like this:
+
+``` markdown
+ * parent item
+
+ * sub-list 1 item 1
+ * sub-list 1 item 2
+ * sub-list 1 item 3
+
+ This will terminate the sub-list 1
+
+ * sub-list 2 item 1
+ * sub-list 2 item 2
+ * sub-list 2 item 3
+```
+
+That's why each item has "subtrees" (plural) instead of singular "subtree". It's up to an interpreter to deal with it. Typical options would be to (1.) use lists separately (2.) only take first list and ignore the rest (3.) concatenate all lists.Write steps to verify multiple nested lists
Also correct and rephrase other parts of the lists spec.
index 1ca4439..c3e5dc0 100644
--- a/spec/lists.md
+++ b/spec/lists.md
@@ -4,27 +4,47 @@ interpreter: "python -m spec.self-check"
=
=# Passing lists to an interpreter
=
-Lists are represented as trees, with each node containing a single value (text) and a subtree. Something like this:
+Lists are represented as trees, with each node containing a single value (text) and a list of sub-trees. Something like this:
=
=``` json
-[
- {
- "value": "Alice",
- "subtrees": [[
- { "value": "Bobby", "subtree": [] },
- {
- "value": "Carol",
- "subtree": [
- { "value": "Damon", "subtree": [] },
- { "value": "Erica", "subtree": [] }
- ]
- }
- ]]
- },
- { "value": "Filon", "subtree": [] }
-]
-```
+{
+ "items": [
+ {
+ "value": "Alice",
+ "subtrees": [
+ {
+ "items": [
+ {
+ "value": "Bobby",
+ "subtrees": []
+ },
+ {
+ "value": "Carol",
+ "subtrees": [
+ {
+ "value": "Damon",
+ "subtrees": []
+ },
+ {
+ "value": "Erica",
+ "subtrees": []
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "value": "Filon",
+ "subtrees": []
+ }
+ ]
+}
+
+<!-- TODO: Move this to `serialization.md` suite -->
=
+```
=## Shallow list
=
= * Store the following list as `the list`
@@ -37,12 +57,12 @@ Lists are represented as trees, with each node containing a single value (text)
= - Carol
=
= * There are `3` items in `the list`
- * The value of item `0` of `the list` is `Alice`
- * The value of item `1` of `the list` is `Bob`
+ * The value of item `0` from `the list` is `Alice`
+ * The value of item `1` from `the list` is `Bob`
=
= Notice that content following the first line is not included.
=
- * The value of item `2` of `the list` is `Carol`
+ * The value of item `2` from `the list` is `Carol`
=
=## Nested list
=
@@ -66,14 +86,14 @@ Lists are represented as trees, with each node containing a single value (text)
= Filona has no children
=
= * There are `2` items in `the first generation`
- * The value of item `0` of `the first generation` is `Alice`
- * The value of item `1` of `the first generation` is `Filona`
- * Take a sub-tree `0` of item `0` of `the first generation` as `the second generaton`
- * The value of item `0` of `the second generation` is `Bobby`
- * The value of item `1` of `the second generation` is `Carol`
- * Take a sub-tree `1` of item `0` of `the second generation` as `the third generaton`
- * The value of item `0` of `the third generation` is `Damon`
- * The value of item `1` of `the third generation` is `Erica`
+ * The value of item `0` from `the first generation` is `Alice`
+ * The value of item `1` from `the first generation` is `Filona`
+ * Take a sub-tree `0` of item `0` from `the first generation` as `the second generation`
+ * The value of item `0` from `the second generation` is `Bobby`
+ * The value of item `1` from `the second generation` is `Carol`
+ * Take a sub-tree `0` of item `1` from `the second generation` as `the third generation`
+ * The value of item `0` from `the third generation` is `Damon`
+ * The value of item `1` from `the third generation` is `Erica`
=
=
=## Multiple nested lists
@@ -95,3 +115,20 @@ It is possible to have more than one list under a single list item, like this:
=```
=
=That's why each item has "subtrees" (plural) instead of singular "subtree". It's up to an interpreter to deal with it. Typical options would be to (1.) use lists separately (2.) only take first list and ignore the rest (3.) concatenate all lists.
+
+ * Store the following list as `the jungle`
+
+ * parent item
+
+ * sub-list 1 item 1
+ * sub-list 1 item 2
+ * sub-list 1 item 3
+
+ This will terminate the sub-list 1
+
+ * sub-list 2 item 1
+ * sub-list 2 item 2
+ * sub-list 2 item 3
+
+ * Take a sub-tree `1` of item `0` from `the jungle` as `the neck`
+ * The value of item `1` from `the neck` is `sub-list 2 item 2`Implement steps for lists spec
index faf374c..bf735fc 100644
--- a/spec/self-check.py
+++ b/spec/self-check.py
@@ -8,7 +8,7 @@ import unittest
=from textwrap import dedent, indent
=
=import spec.tbb as tbb
-from spec.tbb import step, log, indent_tail
+from spec.tbb import step, log, indent_tail, get_at
=
=
=base_command = "tbb"
@@ -18,7 +18,9 @@ if os.environ.get ("CARGO"):
= base_command = cargo_command
=
=tester = unittest.TestCase()
+# TODO: Merge `completed` into the `state` map
=completed = None # Hold results of running a command, for later inspection
+state = {} # Store dynamic variables here
=
=@step("Run the program with {0} command line arguments")
=def step_implementation_00(args: str, **kwargs):
@@ -83,7 +85,7 @@ def step_implementation_03(pattern: str, **kwargs):
=@step("The output will contain {0} block")
=def step_implementation_04(label: str, **kwargs):
= global completed
- block = kwargs['code_blocks'][0]['value']
+ block = get_at(kwargs, ['code_blocks', 0, 'value'])
= output = completed.stdout.decode("utf-8")
=
= # Trim all blank lines and trailing whitespece.
@@ -127,4 +129,35 @@ def step_implementation_05(pattern: str, **kwargs):
= ```
= """)
=
+@step("Store the following list as {0}")
+def step_implementation_06(name: str, **kwargs):
+ global state
+ state[name] = get_at (kwargs, ["lists", 0])
+
+@step("There are {0} items in {1}")
+def step_implementation_07(count: int, list_name: str, **kwargs):
+ global state
+ items = get_at(state, [list_name, "items"])
+ tester.assertEqual(count, len(items))
+
+@step("The value of item {0} from {1} is {2}")
+def step_implementation_08(index: int, list_name: str, expected: str, **kwargs):
+ global state
+
+ dict = {}
+ actual = get_at(state, [list_name, "items", index, "value"])
+ tester.assertEqual(expected, actual)
+
+@step("Take a sub-tree {0} of item {1} from {2} as {3}")
+def step_implementation_09(
+ subtree_index: int,
+ item_index: int,
+ big_list_name: str,
+ small_list_name: str,
+ **kwargs
+):
+ global state
+ subtree = get_at (state, [big_list_name, "items", item_index, "subtrees", subtree_index])
+ state[small_list_name] = subtree
+
=tbb.ready()index 0027c38..b7bf5de 100644
--- a/spec/tbb.py
+++ b/spec/tbb.py
@@ -185,6 +185,25 @@ def ready():
= break
=
=
+# TODO: Docstring and test cases for `get_at`. See `self-check` for example use.
+def get_at(collection, path: [str]):
+ value = collection
+ for key in path:
+ try:
+ value = value[key]
+ except IndexError as error:
+ raise Exception (
+ f"Can't find item at {key}.\n\nThere are {len(value)} items in the list."
+ ) from error
+ except KeyError as error:
+ raise Exception (
+ f"Can't find '{key}' in the dict.\n\nValid keys are {value.keys()}"
+ ) from error
+ except TypeError as error:
+ raise Exception(f"Can't find '{key}'.\n\nThe value is {type(value)}.") from error
+
+ return value
+
=def indent_tail(text: str, indentation: str):
= """Adds indentation to all lines except the first one
=Implement the lists spec
index e023441..885e143 100644
--- a/src/spec.rs
+++ b/src/spec.rs
@@ -63,6 +63,9 @@ pub struct Step {
=
= /// List of tables
= pub tables: Vec<Table>,
+
+ /// List of lists
+ pub lists: Vec<List>,
=}
=
=#[derive(Debug, Serialize, Deserialize, Clone)]
@@ -71,6 +74,18 @@ pub struct CodeBlock {
= language: Option<String>,
= meta: Option<String>,
=}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct List {
+ items: Vec<ListItem>,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct ListItem {
+ pub value: String,
+ pub subtrees: Vec<List>,
+}
+
=impl Spec {
= /// Load suites from a markdown document
= pub fn load_document(&mut self, md: &str) -> anyhow::Result<()> {
@@ -82,6 +97,56 @@ impl Spec {
= }
=}
=
+impl TryFrom<&markdown::mdast::List> for List {
+ type Error = anyhow::Error;
+
+ fn try_from(list: &markdown::mdast::List) -> anyhow::Result<Self> {
+ let items: Vec<ListItem> = list
+ .children
+ .iter()
+ .filter_map(|child| {
+ if let markdown::mdast::Node::ListItem(item) = child {
+ ListItem::try_from(item)
+ .context("extracting a list item from mdast {item:?}")
+ .into()
+ } else {
+ None
+ }
+ })
+ .collect::<anyhow::Result<Vec<ListItem>>>()?; // Bail if any error
+
+ Ok(Self { items })
+ }
+}
+
+impl TryFrom<&markdown::mdast::ListItem> for ListItem {
+ type Error = anyhow::Error;
+
+ fn try_from(item: &markdown::mdast::ListItem) -> anyhow::Result<Self> {
+ let headline = item
+ .children
+ .first()
+ .context("a list without children (how is that even possible?)")?
+ .clone();
+ let subtrees: Vec<List> = item
+ .children
+ .iter()
+ .filter_map(|child| {
+ if let markdown::mdast::Node::List(list) = child {
+ List::try_from(list).into()
+ } else {
+ None
+ }
+ })
+ .collect::<anyhow::Result<Vec<List>>>()?; // Bail if any error
+
+ Ok(Self {
+ value: headline.to_string(),
+ subtrees,
+ })
+ }
+}
+
=#[derive(Debug, Serialize, Deserialize, Clone)]
=pub struct Table(Vec<Vec<String>>);
=
@@ -366,6 +431,7 @@ impl TryFrom<&markdown::mdast::ListItem> for Step {
= // TODO: Extract list, tables, code blocks etc. from subsequent children of the list item
= let mut code_blocks = vec![];
= let mut tables = vec![];
+ let mut lists = vec![];
=
= for child in item.children.iter() {
= match child {
@@ -384,6 +450,11 @@ impl TryFrom<&markdown::mdast::ListItem> for Step {
= tables.push(Table::from(table));
= }
=
+ markdown::mdast::Node::List(list) => {
+ log::debug!("Found a list {list:?}");
+ lists.push(List::try_from(list)?)
+ }
+
= _ => continue,
= }
= }
@@ -394,6 +465,7 @@ impl TryFrom<&markdown::mdast::ListItem> for Step {
= arguments,
= code_blocks,
= tables,
+ lists,
= })
= }
=}Fix grammar
index c3e5dc0..004e5a3 100644
--- a/spec/lists.md
+++ b/spec/lists.md
@@ -114,7 +114,7 @@ It is possible to have more than one list under a single list item, like this:
= * sub-list 2 item 3
=```
=
-That's why each item has "subtrees" (plural) instead of singular "subtree". It's up to an interpreter to deal with it. Typical options would be to (1.) use lists separately (2.) only take first list and ignore the rest (3.) concatenate all lists.
+That's why each item has "subtrees" (plural) instead of a singular "subtree". It's up to an interpreter to deal with it. Typical options would be to (1.) use lists separately (2.) only take first list and ignore the rest (3.) concatenate all lists.
=
= * Store the following list as `the jungle`
=Bump minor version to 0.6.0
To celebrate the implementation of lists feature.
index 16c0e0f..2c22711 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -400,7 +400,7 @@ dependencies = [
=
=[[package]]
=name = "tad-better-behavior"
-version = "0.5.0"
+version = "0.6.0"
=dependencies = [
= "anyhow",
= "clap",index 8d76829..c8b6859 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
=[package]
=name = "tad-better-behavior"
-version = "0.5.0"
+version = "0.6.0"
=edition = "2024"
=
=[dependencies]index 8894299..420e7e4 100644
--- a/README.md
+++ b/README.md
@@ -102,7 +102,7 @@ In summary:
=- [ ] Pass more step data to interpreters
= - [x] Code blocks
= - [x] Tables
- - [ ] Lists
+ - [x] Lists
= - [ ] Definition lists
= - [ ] Original markdown fragment
= - [ ] Block quotes