From 0686b40f979f4607b3fd8cca21c463e98f617666 Mon Sep 17 00:00:00 2001 From: Nathan Reiner Date: Mon, 20 Apr 2026 13:43:01 +0200 Subject: implement builder and instance --- example/build.lua | 9 ++++ example/test.py | 39 ++++++++++++++++ src/arg.lua | 5 ++- src/blueprint | 14 ++++-- src/extensions/init.lua | 1 + src/extensions/string.lua | 86 ++++++++++++++++++++++++++++++++++- src/extensions/table.lua | 28 ++++++++++++ src/lib/builder.lua | 111 +++++++++++++++++++++++++++++++++++++++++++++ src/lib/env.lua | 27 +++++++++++ src/lib/init.lua | 4 ++ src/lib/instance.lua | 54 ++++++++++++++++++++++ src/lib/path.lua | 112 ++++++++++++++++++++++++++++++++++++++++++---- src/lib/template.lua | 51 +++++++++++++++++++++ 13 files changed, 527 insertions(+), 14 deletions(-) create mode 100644 example/build.lua create mode 100644 example/test.py create mode 100644 src/extensions/table.lua create mode 100644 src/lib/builder.lua create mode 100644 src/lib/env.lua create mode 100644 src/lib/instance.lua create mode 100644 src/lib/template.lua 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 -- cgit v1.2.3-70-g09d2