Files
ob-elixir/tasks/03-variable-injection.md

9.1 KiB

Task 03: Variable Injection

Phase: 1 - Core (MVP) Priority: High Estimated Time: 1-2 hours Dependencies: Task 02 (Basic Execution)

Objective

Implement variable injection so that :var header arguments work correctly, allowing data to be passed from org-mode into Elixir code blocks.

Prerequisites

  • Task 02 completed
  • Basic execution working

Background

Org-babel allows passing variables to code blocks:

#+BEGIN_SRC elixir :var x=5 :var name="Alice"
"Hello, #{name}! x = #{x}"
#+END_SRC

We need to:

  1. Convert Elisp values to Elixir syntax
  2. Generate Elixir variable assignment statements
  3. Prepend these to the code before execution

Steps

Step 1: Implement Elisp to Elixir conversion

Add to ob-elixir.el:

;;; Type Conversion

(defun ob-elixir--elisp-to-elixir (value)
  "Convert Elisp VALUE to Elixir literal syntax.

Handles:
- nil -> nil
- t -> true
- numbers -> numbers
- strings -> quoted strings
- symbols -> atoms
- lists -> Elixir lists
- vectors -> tuples"
  (cond
   ;; nil
   ((null value) "nil")
   
   ;; Boolean true
   ((eq value t) "true")
   
   ;; Numbers
   ((numberp value)
    (number-to-string value))
   
   ;; Strings
   ((stringp value)
    (format "\"%s\"" (ob-elixir--escape-string value)))
   
   ;; Symbols become atoms (except special ones)
   ((symbolp value)
    (let ((name (symbol-name value)))
      (cond
       ((string= name "hline") ":hline")
       ((string-match-p "^[a-z_][a-zA-Z0-9_]*[?!]?$" name)
        (format ":%s" name))
       (t (format ":\"%s\"" name)))))
   
   ;; Vectors become tuples
   ((vectorp value)
    (format "{%s}"
            (mapconcat #'ob-elixir--elisp-to-elixir
                       (append value nil) ", ")))
   
   ;; Lists
   ((listp value)
    (format "[%s]"
            (mapconcat #'ob-elixir--elisp-to-elixir value ", ")))
   
   ;; Fallback
   (t (format "%S" value))))

Step 2: Implement string escaping

(defun ob-elixir--escape-string (str)
  "Escape special characters in STR for Elixir string literal."
  (let ((result str))
    ;; Escape backslashes first
    (setq result (replace-regexp-in-string "\\\\" "\\\\\\\\" result))
    ;; Escape double quotes
    (setq result (replace-regexp-in-string "\"" "\\\\\"" result))
    ;; Escape newlines
    (setq result (replace-regexp-in-string "\n" "\\\\n" result))
    ;; Escape tabs
    (setq result (replace-regexp-in-string "\t" "\\\\t" result))
    result))

Step 3: Implement variable assignments function

;;; Variable Handling

(defun org-babel-variable-assignments:elixir (params)
  "Return list of Elixir statements assigning variables from PARAMS.

Each statement has the form: var_name = value"
  (mapcar
   (lambda (pair)
     (let ((name (car pair))
           (value (cdr pair)))
       (format "%s = %s"
               (ob-elixir--var-name name)
               (ob-elixir--elisp-to-elixir value))))
   (org-babel--get-vars params)))

(defun ob-elixir--var-name (name)
  "Convert NAME to a valid Elixir variable name.

Elixir variables must start with lowercase or underscore."
  (let ((str (if (symbolp name) (symbol-name name) name)))
    ;; Ensure starts with lowercase or underscore
    (if (string-match-p "^[a-z_]" str)
        str
      (concat "_" str))))

Step 4: Update the execute function

Modify org-babel-execute:elixir to use variable assignments:

(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.

This function is called by `org-babel-execute-src-block'."
  (let* ((result-type (cdr (assq :result-type params)))
         (result-params (cdr (assq :result-params params)))
         ;; Expand body with variable assignments
         (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
       result
       (org-babel-script-escape 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: Add tests for type conversion

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

;;; Type Conversion Tests

(ert-deftest ob-elixir-test-convert-nil ()
  "Test nil conversion."
  (should (equal "nil" (ob-elixir--elisp-to-elixir nil))))

(ert-deftest ob-elixir-test-convert-true ()
  "Test t conversion."
  (should (equal "true" (ob-elixir--elisp-to-elixir t))))

(ert-deftest ob-elixir-test-convert-integer ()
  "Test integer conversion."
  (should (equal "42" (ob-elixir--elisp-to-elixir 42)))
  (should (equal "-10" (ob-elixir--elisp-to-elixir -10))))

(ert-deftest ob-elixir-test-convert-float ()
  "Test float conversion."
  (should (equal "3.14" (ob-elixir--elisp-to-elixir 3.14))))

(ert-deftest ob-elixir-test-convert-string ()
  "Test string conversion."
  (should (equal "\"hello\"" (ob-elixir--elisp-to-elixir "hello"))))

(ert-deftest ob-elixir-test-convert-string-escaping ()
  "Test string escaping."
  (should (equal "\"say \\\"hi\\\"\"" 
                 (ob-elixir--elisp-to-elixir "say \"hi\"")))
  (should (equal "\"line1\\nline2\"" 
                 (ob-elixir--elisp-to-elixir "line1\nline2"))))

(ert-deftest ob-elixir-test-convert-symbol ()
  "Test symbol conversion to atom."
  (should (equal ":foo" (ob-elixir--elisp-to-elixir 'foo)))
  (should (equal ":ok" (ob-elixir--elisp-to-elixir 'ok))))

(ert-deftest ob-elixir-test-convert-list ()
  "Test list conversion."
  (should (equal "[1, 2, 3]" (ob-elixir--elisp-to-elixir '(1 2 3))))
  (should (equal "[\"a\", \"b\"]" (ob-elixir--elisp-to-elixir '("a" "b")))))

(ert-deftest ob-elixir-test-convert-nested-list ()
  "Test nested list conversion."
  (should (equal "[[1, 2], [3, 4]]" 
                 (ob-elixir--elisp-to-elixir '((1 2) (3 4))))))

(ert-deftest ob-elixir-test-convert-vector ()
  "Test vector to tuple conversion."
  (should (equal "{1, 2, 3}" (ob-elixir--elisp-to-elixir [1 2 3]))))

Step 6: Add tests for variable injection

;;; Variable Injection Tests

(ert-deftest ob-elixir-test-variable-assignments ()
  "Test variable assignment generation."
  (let ((params '((:var . ("x" . 5))
                  (:var . ("name" . "Alice")))))
    (let ((assignments (org-babel-variable-assignments:elixir params)))
      (should (member "x = 5" assignments))
      (should (member "name = \"Alice\"" assignments)))))

(ert-deftest ob-elixir-test-var-execution ()
  "Test code execution with variables."
  (skip-unless (executable-find ob-elixir-command))
  (let* ((params '((:var . ("x" . 10))))
         (var-lines (org-babel-variable-assignments:elixir params))
         (full-body (concat (mapconcat #'identity var-lines "\n")
                            "\nx * 2")))
    (should (equal "20" (ob-elixir--execute full-body 'value)))))

(ert-deftest ob-elixir-test-var-list ()
  "Test passing list as variable."
  (skip-unless (executable-find ob-elixir-command))
  (let* ((params '((:var . ("data" . (1 2 3)))))
         (var-lines (org-babel-variable-assignments:elixir params))
         (full-body (concat (mapconcat #'identity var-lines "\n")
                            "\nEnum.sum(data)")))
    (should (equal "6" (ob-elixir--execute full-body 'value)))))

Step 7: Test in an org buffer

Update test.org:

* Variable Injection Tests

** Simple variable

#+BEGIN_SRC elixir :var x=42
x * 2
#+END_SRC

#+RESULTS:
: 84

** String variable

#+BEGIN_SRC elixir :var name="World"
"Hello, #{name}!"
#+END_SRC

#+RESULTS:
: Hello, World!

** Multiple variables

#+BEGIN_SRC elixir :var x=10 :var y=20
x + y
#+END_SRC

#+RESULTS:
: 30

** List variable

#+BEGIN_SRC elixir :var numbers='(1 2 3 4 5)
Enum.sum(numbers)
#+END_SRC

#+RESULTS:
: 15

** Table as variable

#+NAME: my-data
| a | 1 |
| b | 2 |
| c | 3 |

#+BEGIN_SRC elixir :var data=my-data
Enum.map(data, fn [k, v] -> "#{k}=#{v}" end)
#+END_SRC

Acceptance Criteria

  • ob-elixir--elisp-to-elixir correctly converts all Elisp types
  • org-babel-variable-assignments:elixir generates valid Elixir code
  • :var x=5 works in org blocks
  • :var name="string" works with string values
  • Multiple :var arguments work
  • Lists and tables can be passed as variables
  • All tests pass: make test

Edge Cases to Consider

  1. Variable name conflicts: Elixir variables must start with lowercase
  2. Special characters in strings: Quotes, newlines, backslashes
  3. Empty lists: Should produce []
  4. Mixed type lists: [1, "two", :three]
  5. hline in tables: Special symbol for table separators

Files Modified

  • ob-elixir.el - Add variable handling functions
  • test/test-ob-elixir.el - Add variable tests

References