aboutsummaryrefslogtreecommitdiff
path: root/src/lib
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 /src/lib
parentba561ebd063b391013b6c9c1fcc9b1838dd422e6 (diff)
implement builder and instance
Diffstat (limited to 'src/lib')
-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
6 files changed, 350 insertions, 9 deletions
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