Files
ob-elixir/tasks/05-result-formatting.md

9.0 KiB

Task 05: Result Formatting and Table Support

Phase: 1 - Core (MVP) Priority: Medium Estimated Time: 1-2 hours Dependencies: Task 02 (Basic Execution), Task 03 (Variable Injection)

Objective

Implement proper result formatting so Elixir lists become org tables and results are properly parsed back into Elisp data structures.

Prerequisites

  • Task 02 and 03 completed
  • Basic execution and variables working

Background

Org-babel can display results as:

  • Scalar values (:results scalar)
  • Tables (:results table)
  • Raw org markup (:results raw)
  • Verbatim (:results verbatim)

When Elixir returns a list like [[1, 2], [3, 4]], org should display it as a table:

| 1 | 2 |
| 3 | 4 |

Steps

Step 1: Implement result parsing

Add to ob-elixir.el:

;;; Result Formatting

(defun ob-elixir--table-or-string (result)
  "Convert RESULT to Emacs table or string.

If RESULT looks like a list, parse it into an Elisp list.
Otherwise return as string.

Uses `org-babel-script-escape' for parsing."
  (let ((trimmed (string-trim result)))
    (cond
     ;; Empty result
     ((string-empty-p trimmed) nil)
     
     ;; Looks like a list - try to parse
     ((string-match-p "^\\[.*\\]$" trimmed)
      (condition-case nil
          (let ((parsed (org-babel-script-escape trimmed)))
            (ob-elixir--sanitize-table parsed))
        (error trimmed)))
     
     ;; Looks like a tuple - convert to list first
     ((string-match-p "^{.*}$" trimmed)
      (condition-case nil
          (let* ((as-list (replace-regexp-in-string
                           "^{\\(.*\\)}$" "[\\1]" trimmed))
                 (parsed (org-babel-script-escape as-list)))
            (ob-elixir--sanitize-table parsed))
        (error trimmed)))
     
     ;; Scalar value
     (t trimmed))))

Step 2: Implement table sanitization

(defvar ob-elixir-nil-to 'hline
  "Elisp value to use for Elixir nil in table cells.

When nil appears in an Elixir list that becomes a table,
it is replaced with this value. Use `hline' for org table
horizontal lines, or nil for empty cells.")

(defun ob-elixir--sanitize-table (data)
  "Sanitize DATA for use as an org table.

Replaces nil values according to `ob-elixir-nil-to'.
Ensures consistent structure for table rendering."
  (cond
   ;; Not a list - return as-is
   ((not (listp data)) data)
   
   ;; Empty list
   ((null data) nil)
   
   ;; List of lists - could be table
   ((and (listp (car data)) (not (null (car data))))
    (mapcar #'ob-elixir--sanitize-row data))
   
   ;; Simple list - single row
   (t (ob-elixir--sanitize-row data))))

(defun ob-elixir--sanitize-row (row)
  "Sanitize a single ROW for table display."
  (if (listp row)
      (mapcar (lambda (cell)
                (cond
                 ((null cell) ob-elixir-nil-to)
                 ((eq cell 'nil) ob-elixir-nil-to)
                 (t cell)))
              row)
    row))

Step 3: Handle keyword lists and maps

(defun ob-elixir--parse-keyword-list (str)
  "Parse STR as Elixir keyword list into alist.

Handles format like: [a: 1, b: 2]"
  (when (string-match "^\\[\\(.*\\)\\]$" str)
    (let ((content (match-string 1 str)))
      (when (string-match-p "^[a-z_]+:" content)
        (let ((pairs '()))
          (dolist (part (split-string content ", "))
            (when (string-match "^\\([a-z_]+\\):\\s-*\\(.+\\)$" part)
              (push (cons (intern (match-string 1 part))
                          (ob-elixir--parse-value (match-string 2 part)))
                    pairs)))
          (nreverse pairs))))))

(defun ob-elixir--parse-value (str)
  "Parse STR as a simple Elixir value."
  (let ((trimmed (string-trim str)))
    (cond
     ((string= trimmed "nil") nil)
     ((string= trimmed "true") t)
     ((string= trimmed "false") nil)
     ((string-match-p "^[0-9]+$" trimmed)
      (string-to-number trimmed))
     ((string-match-p "^[0-9]+\\.[0-9]+$" trimmed)
      (string-to-number trimmed))
     ((string-match-p "^\".*\"$" trimmed)
      (substring trimmed 1 -1))
     ((string-match-p "^:.*$" trimmed)
      (intern (substring trimmed 1)))
     (t trimmed))))

Step 4: Update the execute function

Ensure org-babel-execute:elixir uses the parsing:

(defun org-babel-execute:elixir (body params)
  "Execute a block of Elixir code with org-babel.

BODY is the Elixir code to execute.
PARAMS is an alist of header arguments."
  (let* ((result-type (cdr (assq :result-type params)))
         (result-params (cdr (assq :result-params params)))
         (full-body (org-babel-expand-body:generic
                     body params
                     (org-babel-variable-assignments:elixir params)))
         (result (ob-elixir--execute full-body result-type)))
    (org-babel-reassemble-table
     (org-babel-result-cond result-params
       ;; For output/scalar/verbatim - return as-is
       result
       ;; For value - parse into Elisp data
       (ob-elixir--table-or-string result))
     (org-babel-pick-name (cdr (assq :colname-names params))
                          (cdr (assq :colnames params)))
     (org-babel-pick-name (cdr (assq :rowname-names params))
                          (cdr (assq :rownames params))))))

Step 5: Support column names

(defun ob-elixir--maybe-add-colnames (result params)
  "Add column names to RESULT if specified in PARAMS."
  (let ((colnames (cdr (assq :colnames params))))
    (if (and colnames (listp result) (listp (car result)))
        (cons (if (listp colnames) colnames (car result))
              (if (listp colnames) result (cdr result)))
      result)))

Step 6: Add tests

Add to test/test-ob-elixir.el:

;;; Result Formatting Tests

(ert-deftest ob-elixir-test-parse-simple-list ()
  "Test parsing simple list result."
  (should (equal '(1 2 3) (ob-elixir--table-or-string "[1, 2, 3]"))))

(ert-deftest ob-elixir-test-parse-nested-list ()
  "Test parsing nested list (table) result."
  (should (equal '((1 2) (3 4))
                 (ob-elixir--table-or-string "[[1, 2], [3, 4]]"))))

(ert-deftest ob-elixir-test-parse-tuple ()
  "Test parsing tuple result."
  (should (equal '(1 2 3) (ob-elixir--table-or-string "{1, 2, 3}"))))

(ert-deftest ob-elixir-test-parse-scalar ()
  "Test that scalars are returned as strings."
  (should (equal "42" (ob-elixir--table-or-string "42")))
  (should (equal ":ok" (ob-elixir--table-or-string ":ok"))))

(ert-deftest ob-elixir-test-parse-string ()
  "Test parsing string result."
  (should (equal "\"hello\"" (ob-elixir--table-or-string "\"hello\""))))

(ert-deftest ob-elixir-test-sanitize-table-nil ()
  "Test that nil values are sanitized in tables."
  (let ((ob-elixir-nil-to 'hline))
    (should (equal '((1 hline) (hline 2))
                   (ob-elixir--sanitize-table '((1 nil) (nil 2)))))))

(ert-deftest ob-elixir-test-execution-returns-table ()
  "Test that list results become tables."
  (skip-unless (executable-find ob-elixir-command))
  (let ((result (ob-elixir--table-or-string
                 (ob-elixir--execute "[[1, 2], [3, 4]]" 'value))))
    (should (equal '((1 2) (3 4)) result))))

(ert-deftest ob-elixir-test-mixed-list ()
  "Test parsing mixed-type list."
  (skip-unless (executable-find ob-elixir-command))
  (let ((result (ob-elixir--table-or-string
                 (ob-elixir--execute "[1, \"two\", :three]" 'value))))
    (should (listp result))
    (should (= 3 (length result)))))

Step 7: Test in an org buffer

Add to test.org:

* Result Formatting Tests

** Simple list as table row

#+BEGIN_SRC elixir
[1, 2, 3, 4, 5]
#+END_SRC

#+RESULTS:
| 1 | 2 | 3 | 4 | 5 |

** Nested list as table

#+BEGIN_SRC elixir
[
  ["Alice", 30],
  ["Bob", 25],
  ["Charlie", 35]
]
#+END_SRC

#+RESULTS:
| Alice   | 30 |
| Bob     | 25 |
| Charlie | 35 |

** Map result (verbatim)

#+BEGIN_SRC elixir :results verbatim
%{name: "Alice", age: 30}
#+END_SRC

#+RESULTS:
: %{age: 30, name: "Alice"}

** Enum operations returning lists

#+BEGIN_SRC elixir
Enum.map(1..5, fn x -> [x, x * x] end)
#+END_SRC

#+RESULTS:
| 1 |  1 |
| 2 |  4 |
| 3 |  9 |
| 4 | 16 |
| 5 | 25 |

** Tuple result

#+BEGIN_SRC elixir
{:ok, "success", 123}
#+END_SRC

Acceptance Criteria

  • Simple lists [1, 2, 3] become table rows
  • Nested lists [[1, 2], [3, 4]] become tables
  • Tuples are handled (converted to lists)
  • Scalar values remain as strings
  • :results verbatim bypasses table conversion
  • nil values in tables are handled according to config
  • All tests pass: make test

Result Format Reference

Elixir Value Org Display
[1, 2, 3] | 1 | 2 | 3 |
[[1], [2]] Multi-row table
{:ok, 1} | ok | 1 |
42 : 42
"hello" : "hello"
%{a: 1} : %{a: 1}

Files Modified

  • ob-elixir.el - Add result formatting functions
  • test/test-ob-elixir.el - Add formatting tests

References