335 lines
9.1 KiB
Markdown
335 lines
9.1 KiB
Markdown
# 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:
|
|
|
|
```org
|
|
#+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`:
|
|
|
|
```elisp
|
|
;;; 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
|
|
|
|
```elisp
|
|
(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
|
|
|
|
```elisp
|
|
;;; 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:
|
|
|
|
```elisp
|
|
(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`:
|
|
|
|
```elisp
|
|
;;; 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
|
|
|
|
```elisp
|
|
;;; 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`:
|
|
|
|
```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
|
|
|
|
- [docs/03-org-babel-implementation-guide.md](../docs/03-org-babel-implementation-guide.md) - Variable Handling section
|
|
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - Data Type Conversion section
|