aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Reiner <nathan@nathanreiner.xyz>2026-04-20 13:43:01 +0200
committerNathan Reiner <nathan@nathanreiner.xyz>2026-04-20 13:43:01 +0200
commit0686b40f979f4607b3fd8cca21c463e98f617666 (patch)
tree355e8a7bfd4b37510d05370876a9d8cedfdcd5d7
parentba561ebd063b391013b6c9c1fcc9b1838dd422e6 (diff)
implement builder and instance
-rw-r--r--example/build.lua9
-rw-r--r--example/test.py39
-rw-r--r--src/arg.lua5
-rwxr-xr-xsrc/blueprint14
-rw-r--r--src/extensions/init.lua1
-rw-r--r--src/extensions/string.lua86
-rw-r--r--src/extensions/table.lua28
-rw-r--r--src/lib/builder.lua111
-rw-r--r--src/lib/env.lua27
-rw-r--r--src/lib/init.lua4
-rw-r--r--src/lib/instance.lua54
-rw-r--r--src/lib/path.lua112
-rw-r--r--src/lib/template.lua51
13 files changed, 527 insertions, 14 deletions
diff --git a/example/build.lua b/example/build.lua
new file mode 100644
index 0000000..47cfbda
--- /dev/null
+++ b/example/build.lua
@@ -0,0 +1,9 @@
+
+local template = Template:new()
+
+template:add_file("./test.py", {
+ module = "a_module_that_is_different",
+ name = template:option('name')
+})
+
+return template
diff --git a/example/test.py b/example/test.py
new file mode 100644
index 0000000..20c1c7e
--- /dev/null
+++ b/example/test.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+
+# This module is called @{name()}
+
+import @{module}
+
+print("@{function()
+ return "Hello World"
+end}")
+
+print("@{function()
+ local a = { "asdf", "1234" }
+ return a[1]
+end}")
+
+print(@{function()
+ local a = { "asdf", "1234" } -- {
+ return a[2]
+end})
+
+print("@{function()
+ local a = "asdf{asdf"
+ return a:sub(1)
+end}")
+
+print("@{function()
+ local a = "asdf"
+ --[[
+ some comment }
+ --]]
+ return a
+end}")
+
+print("@{function()
+ local a = [[
+ some comment }
+ ]]
+ return a:sub(2, 8)
+end}")
diff --git a/src/arg.lua b/src/arg.lua
index 1254b71..de19c77 100644
--- a/src/arg.lua
+++ b/src/arg.lua
@@ -82,9 +82,12 @@ return function (opts)
else
local option = params[flag]
- if option == nil then
+ if option == nil and opts.strict then
print("error: unknown flag '" .. flag .. "'")
help(opts)
+ elseif option == nil then
+ option = { name = flag, kind = 'property' }
+ params[flag] = option
end
if option.kind == 'flag' then
diff --git a/src/blueprint b/src/blueprint
index 41b2162..871105d 100755
--- a/src/blueprint
+++ b/src/blueprint
@@ -13,8 +13,16 @@ require('extensions')
require('lib')
local args = require('arg') {
- { name = "path" },
-
+ strict = false,
+ { name = "target" },
{ name = "template", description = "some description", kind = "property", required = true },
- { name = "flag", description = "other description", kind = "flag" },
}
+
+local template = loadfile('./example/build.lua', nil, Env.Sandbox {
+ Template = Template
+})
+
+local builder = Builder:from_template('./example/build.lua')
+
+local instance = builder:build(args)
+instance:save_to(args.target)
diff --git a/src/extensions/init.lua b/src/extensions/init.lua
index a4c4f5a..ba3c928 100644
--- a/src/extensions/init.lua
+++ b/src/extensions/init.lua
@@ -1 +1,2 @@
require('extensions.string')
+require('extensions.table')
diff --git a/src/extensions/string.lua b/src/extensions/string.lua
index 247d31a..5cbb4ec 100644
--- a/src/extensions/string.lua
+++ b/src/extensions/string.lua
@@ -1,3 +1,7 @@
+function string:starts_with(s)
+ return self:sub(1, #s) == s
+end
+
function string:flag_name()
if self:sub(1, 2) ~= "--" then
return nil
@@ -16,11 +20,91 @@ function string:split(sep)
return splits
end
+-- NOTE: This function is for certain not complete and not very efficient
+-- but for now it works good enough to proceed with development.
+-- We might need to rewrite this function in the future if it is a bottleneck.
+function string:split_into_template_blocks()
+ local splits = { { kind = 'text', content = '' } }
+ local stack = {}
+ local current = self
+
+ local matching = {
+ ["{"] = "}",
+ ["["] = "]",
+ ["("] = ")",
+ }
+
+ while #current > 0 do
+ local add = true
+ if #stack == 0 and current:starts_with("@{") then
+ current = current:sub(2, #current)
+ stack[#stack + 1] = "}"
+ splits[#splits + 1] = { kind = 'code', content = '' }
+ add = false
+ elseif #stack > 0 and (
+ current:starts_with('"') or
+ current:starts_with("'") or
+ current:starts_with('[[')
+ ) then
+ local string_end = current:starts_with('"') and '"' or (current:starts_with("'") and "'" or ']]')
+ splits[#splits].content = splits[#splits].content .. current:sub(1, 1)
+ current = current:sub(2)
+
+ while #current > 0 do
+ if current:starts_with('\\') then
+ splits[#splits].content = splits[#splits].content .. current:sub(1, 1)
+ current = current:sub(2)
+ elseif current:starts_with(string_end) then
+ break
+ end
+ splits[#splits].content = splits[#splits].content .. current:sub(1, 1)
+ current = current:sub(2)
+ end
+
+ elseif #stack > 0 and current:starts_with("--") then
+ local comment_end = current:starts_with('--[[') and ']]' or '\n'
+ splits[#splits].content = splits[#splits].content .. current:sub(1, (current:find(comment_end) + #comment_end) or #current + 1)
+ current = current:sub((current:find(comment_end) + #comment_end) or #current + 1)
+ add = false
+ elseif #stack > 0 then
+ local start = current:sub(1, 1)
+ local closing = matching[start]
+
+ if start == stack[#stack] then
+ stack[#stack] = nil
+
+ if #stack == 0 then
+ current = current:sub(2, #current)
+ splits[#splits + 1] = { kind = 'text', content = '' }
+ end
+ elseif closing then
+ stack[#stack + 1] = closing
+ end
+ end
+
+ if add then
+ splits[#splits].content = splits[#splits].content .. current:sub(1, 1)
+ end
+ current = current:sub(2)
+ end
+
+ return splits
+end
+
return {
tests = {
function()
local path = 'some/path/here'
- assert.equals({ 'some', 'path', 'here' }, path.split("/"))
+ assert.equals({ 'some', 'path', 'here' }, path:split("/"))
end,
+ function()
+ local f = Path:new("example/test.py"):open("r")
+ local raw = f:read("*all")
+ f:close()
+
+ for _, value in ipairs(raw:split_into_template_blocks()) do
+ print(require('inspect')(value))
+ end
+ end
}
}
diff --git a/src/extensions/table.lua b/src/extensions/table.lua
new file mode 100644
index 0000000..eb983d3
--- /dev/null
+++ b/src/extensions/table.lua
@@ -0,0 +1,28 @@
+
+function table:extend(other)
+ if type(other) == 'table' then
+ for _, value in ipairs(other) do
+ self[#self + 1] = value
+ end
+ else
+ for value in other do
+ self[#self + 1] = value
+ end
+ end
+end
+
+function table:merge(other)
+ for key, value in pairs(other) do
+ self[key] = value
+ end
+end
+
+function table.from_iter(iter)
+ local t = {}
+
+ for value in iter do
+ t[#t + 1] = value
+ end
+
+ return t
+end
diff --git a/src/lib/builder.lua b/src/lib/builder.lua
new file mode 100644
index 0000000..7390257
--- /dev/null
+++ b/src/lib/builder.lua
@@ -0,0 +1,111 @@
+Builder = {}
+
+function Builder:from_template(path)
+ local builder = {
+ template = loadfile(
+ path,
+ nil,
+ Env.Sandbox {
+ Template = Template
+ }
+ )(),
+ source = Path:new(path),
+ }
+
+ builder.source_directory = builder.source:parent()
+
+ setmetatable(builder, self)
+ self.__index = self
+
+ local argument_template = {
+ strict = true,
+ { name = "target" },
+ { name = "template", description = "some description", kind = "property", required = true },
+ };
+
+ for key, value in pairs(builder.template.options) do
+ argument_template[#argument_template + 1] = value
+ end
+
+ local args = require('arg')(argument_template)
+
+ for key, _ in pairs(builder.template.options) do
+ builder.template.arguments[key] = args[key]
+ end
+
+ return builder
+end
+
+function Builder:entries()
+ local entries = {}
+
+ for _, value in ipairs(self.template.files) do
+ entries[#entries + 1] = {
+ path = Path:new(value.path),
+ env = value.env,
+ }
+ end
+
+ for _, value in ipairs(self.template.directories) do
+ for entry in Path:new(value.path):children() do
+ entries[#entries + 1] = {
+ path = entry,
+ env = value.env,
+ }
+ end
+ end
+
+ local i = 1
+ return function()
+ local value = entries[i]
+ i = i + 1
+ return value
+ end
+end
+
+function Builder:build(args)
+ local instance = Instance:new()
+
+ for entry in self:entries() do
+ instance:register(
+ entry.path,
+ entry.path.is_directory and nil or self:process_file(entry)
+ )
+ end
+
+ return instance
+end
+
+function Builder:process_file(file)
+ local f = (self.source_directory / file.path):open("r")
+ local raw = f:read("*all")
+ f:close()
+
+ local content = ''
+
+ for _, split in ipairs(raw:split_into_template_blocks()) do
+ if split.kind == 'text' then
+ content = content .. split.content
+ else
+ local func, err = load(
+ 'return ' .. split.content,
+ 'macro ' .. tostring(file.path),
+ 't',
+ Env.Sandbox(file.env)
+ )
+
+ if err then
+ error(err)
+ end
+
+ local value = func()
+ if type(value) == 'function' then
+ content = content .. tostring(value())
+ else
+ content = content .. tostring(value)
+ end
+ end
+ end
+
+ return content
+end
diff --git a/src/lib/env.lua b/src/lib/env.lua
new file mode 100644
index 0000000..fd55444
--- /dev/null
+++ b/src/lib/env.lua
@@ -0,0 +1,27 @@
+Env = {}
+
+function Env.Sandbox(opts)
+ local env = {
+ string = string,
+ table = table,
+ type = type,
+ utf8 = utf8,
+ pairs = pairs,
+ ipairs = ipairs,
+ select = select,
+ tonumber = tonumber,
+ tostring = tostring,
+ next = next,
+ math = math,
+ error = error,
+ getmetatable = getmetatable,
+ }
+
+ opts = opts or {}
+
+ for key, value in pairs(opts) do
+ env[key] = value
+ end
+
+ return env
+end
diff --git a/src/lib/init.lua b/src/lib/init.lua
index 4f069d0..2db3367 100644
--- a/src/lib/init.lua
+++ b/src/lib/init.lua
@@ -1 +1,5 @@
require('lib.path')
+require('lib.env')
+require('lib.template')
+require('lib.builder')
+require('lib.instance')
diff --git a/src/lib/instance.lua b/src/lib/instance.lua
new file mode 100644
index 0000000..2de2c5f
--- /dev/null
+++ b/src/lib/instance.lua
@@ -0,0 +1,54 @@
+Instance = {}
+
+function Instance:new()
+ local instance = {
+ entries = {},
+ }
+
+ setmetatable(instance, self)
+ self.__index = self
+
+ return instance
+end
+
+function Instance:register(path, content)
+ self.entries[#self.entries + 1] = {
+ path = path,
+ content = content,
+ }
+end
+
+function Instance:save_to(target)
+ for _, entry in ipairs(self.entries) do
+ local target_entry = Path:new(target) / entry.path
+
+ if entry.content == nil then
+ target_entry:make_directory { create_parents = true }
+ else
+ target_entry:parent():make_directory { create_parents = true }
+ local file = target_entry:open('w')
+ file:write(entry.content)
+ file:close()
+ end
+ end
+end
+
+function Instance:__tostring()
+ local output = [[{
+ entries = {
+]]
+
+ for _, entry in ipairs(self.entries) do
+ output = output .. [[ {
+ path = ]] .. tostring(entry.path) .. [[,
+ content = ]] .. (entry.content and ("[[\n " .. entry.content:gsub('\n', '\n ') .. "\n ]]") or "nil") .. [[,
+ },
+]]
+ end
+
+ output = output .. [[ }
+}
+]]
+
+ return output
+end
diff --git a/src/lib/path.lua b/src/lib/path.lua
index b0f0fac..f99585b 100644
--- a/src/lib/path.lua
+++ b/src/lib/path.lua
@@ -1,7 +1,18 @@
Path = {}
function Path:new(p)
- local path = { segments = p:split('/') }
+ local is_absolute = p:sub(1, 1) == "/"
+ p = p:gsub("/+", "/")
+ p = p:gsub("(/%./", "/")
+ p = p:gsub("/%.$", "")
+ p = p:gsub("/$", "")
+ p = p:gsub("^%./", "")
+ p = p:gsub("/[^/]+/%.%.", "")
+
+ local path = {
+ segments = p:split('/'),
+ is_absolute = is_absolute
+ }
setmetatable(path, self)
self.__index = self
@@ -18,20 +29,87 @@ function Path:stem()
end
function Path:parent()
- local path = { segments = { table.unpack(self.segments, 1, #self.segments - 1) } }
-
- setmetatable(path, self)
- self.__index = self
-
- return path
+ return self / ".."
end
function Path:__tostring()
- return table.concat(self.segments, "/")
+ return (self.is_absolute and "/" or "") .. table.concat(self.segments, "/")
end
function Path:__div(next)
- return Path:new(self:__tostring() .. "/" .. next)
+ if next.is_absolute then
+ error("cannot concat with absolute path")
+ end
+
+ return Path:new(self:__tostring() .. "/" .. tostring(next))
+end
+
+function Path:open(mode)
+ return io.open(tostring(self), mode)
+end
+
+function Path:exists()
+ local f = self:open('r')
+
+ if f ~= nil then
+ f:close()
+ return true
+ end
+ return false
+end
+
+function Path:is_directory()
+ local f = self:open('r')
+
+ if not f then
+ return false
+ end
+
+ local value, err, code = f:read()
+ f:close()
+
+ return code == 21
+end
+
+function Path:entries()
+ if not self:is_directory() then
+ return function() end
+ end
+
+ local pipe = io.popen('ls' .. ' "' .. tostring(self) .. '"')
+ local lines = pipe:lines()
+
+ return function()
+ local value = lines()
+ if value == nil then
+ pipe:close()
+ return nil
+ end
+ return self / value
+ end
+end
+
+function Path:children()
+ if not self:is_directory() then
+ return function() end
+ end
+
+ local pipe = io.popen('find' .. ' "' .. tostring(self) .. '"')
+ local lines = pipe:lines()
+
+ return function()
+ local value = lines()
+ if value == nil then
+ pipe:close()
+ return nil
+ end
+ return Path:new(value)
+ end
+end
+
+function Path:make_directory(opts)
+ local pipe = io.popen('mkdir ' .. (opts.create_parents and '-p ' or ' ') .. tostring(self))
+ pipe:close()
end
return {
@@ -56,6 +134,22 @@ return {
local path = Path:new("some/path/here")
path = path / 'next.txt'
assert.equals("some/path/here/next.txt", path:__tostring())
+ end,
+ function()
+ local path = Path:new("./src")
+ assert.equals(true, path:is_directory())
+ end,
+ function()
+ local path = Path:new("./src/blueprint")
+ assert.equals(false, path:is_directory())
+ end,
+ function()
+ local path = Path:new("./src/blueprint")
+ assert.equals(true, path:exists())
+ end,
+ function()
+ local path = Path:new("./src/blueprint__")
+ assert.equals(false, path:exists())
end
}
}
diff --git a/src/lib/template.lua b/src/lib/template.lua
new file mode 100644
index 0000000..d1f3a94
--- /dev/null
+++ b/src/lib/template.lua
@@ -0,0 +1,51 @@
+Template = {}
+
+function Template:new()
+ local template = {
+ directories = {},
+ files = {},
+ options = {},
+ arguments = {}
+ }
+
+ setmetatable(template, self)
+ self.__index = self
+
+ return template
+end
+
+function Template:set_option(name, description, default, required)
+ required = required == nil or required
+ self.options[name] = {
+ name = name,
+ default = default,
+ required = required,
+ kind = 'property',
+ description = description,
+ }
+
+end
+
+function Template:option(name)
+ if not self.options[name] then
+ self:set_option(name)
+ end
+
+ return function()
+ return self.arguments[name]
+ end
+end
+
+function Template:add_directory(path, env)
+ self.directories[#self.directories + 1] = {
+ path = path,
+ env = env,
+ }
+end
+
+function Template:add_file(path, env)
+ self.files[#self.files + 1] = {
+ path = path,
+ env = env,
+ }
+end