Remove teeplate dependency for builtin solution #1812
-
PremiseWhile on my quest to add Windows support to Lucky framework, I've run into a bit of a snag with one our dependencies: teeplate. Teeplate is actually a fork of this shard, but it hasn't been updated in some time now. It also does not currently support Windows. When diving into our fork's implementation to potentially add Windows support, I quickly realized this would be a harder task than I initially thought. I asked on Discord whether teeplate was worth the effort of refactoring/rewriting, or if we could instead leverage a simpler, builtin solution (maybe something that only relies on the stdlib?). The response was positive to look into a builtin solution to remove this dependency. The following suggestion is my idea of a builtin solution. Builtin SolutionMy idea for making a simpler version of teeplate can be best thought of in two parts, but only one of them requires an implementation on our part. Template filesThe first part is: how do we craft templates? We use ECR, which will define a Directory structureThe second part is: how do we define a directory structure the same way teeplate did? My idea for this is to make it in code instead of in the repo. This has a few benefits:
Pseudocode of a possible implementationExample folder structure from teeplate:
Possible implementation: require "ecr"
class ShardYml
ECR.def_to_s "shard.yml.ecr"
end
class Readme
ECR.def_to_s "README.md.ecr"
end
class License
ECR.def_to_s "LICENSE.ecr"
end
class MyCustomFile
def initialize(@name : String)
end
ECR.def_to_s "my_custom_file.cr.ecr"
end
custom_file_name = "lucky_template"
tree = LuckyTemplate.create_tree(Path["./templates"]) do |folder|
folder.add_file("shard.yml", ShardYml.new)
folder.add_file("README.md", Readme.new)
folder.add_file("LICENSE", License.new)
folder.add_folder("src") do |src|
src.add_file("#{custom_file_name}.cr", MyCustomFile.new(custom_file_name))
src.add_folder(custom_file_name) do |cfn|
cfn.add_file("version.cr", <<-VERSION)
module #{custom_file_name.camelcase}
VERSION = "0.1.0"
end
VERSION
end
end
end
tree.create! # will actually create the files/folders in ./templates directory At the moment, it's a little verbose, but that's just to show off the intent of it. It's meant to be a simpler solution, almost purpose built, using Crystal's stdlib. It won't have a ton of bells and whistles up front, but I think this can provide most of what we need to get going. |
Beta Was this translation helpful? Give feedback.
Replies: 5 comments 12 replies
-
I love this idea. One thing I'd like to try and keep in mind for this is ensuring the resulting code always compiles and can pass both a format and ameba checks. Though, generated apps don't come with ameba by default, they still get tested against currently being separate files. So for example, we go this route, then down the road someone makes a change to a file, but with an extra space somewhere, or some syntax error, we need to make sure that the resulting apps fully catch all of that during the CI builds. Related: luckyframework/lucky_cli#530 |
Beta Was this translation helpful? Give feedback.
-
Teeplate has been in desperate need of being replaced for a long time, and this seems like a great opportunity to improve the lucky generators overall. Here are some things I learned in working on both Amber and Lucky:
|
Beta Was this translation helpful? Give feedback.
-
One of the current messes in the Lucky Generators is the compositional design. If memory serves right, you can generate these permutations:
These are helpful constructs but the pattern itself doesn't scale and maintaining the generators is already an art -- "wait where does this line in particular come from?" is a question I ask frequently. Building them in code as you've suggested could help this a lot. At least, I'm thinking you can use class inheritance to your aid. But another pattern exists in full template repositories like lucky jumpstart. I'm not yet suggesting to move everything into template repos, but maybe there are lessons to be learned there. (Posting this as a different response because it's in a totally different vein.) |
Beta Was this translation helpful? Give feedback.
-
UPDATEI have been working on a POC (proof-of-concept) that implements some of the ideas in my initial suggestion in the discussion, and I'm ready to share what I've created. Behold... What is LuckyTemplate?LuckyTemplate is very similar to Teeplate in its' goals, however, it's much simpler and a lot more capable. Here are some of the major points:
What does LuckyTemplate look like?Simply put, this is the smallest example of what LuckyTemplate looks like: require "lucky_template"
LuckyTemplate.write!(Path["."]) { } However, let's see something more interesting. README.md.ecr
lucky_template_example.cr require "lucky_template"
class Readme
include LuckyTemplate::Fileable
def initialize(@name : String)
end
def to_file(io : IO) : Nil
to_s(io)
end
ECR.def_to_s "#{__DIR__}/README.md.ecr"
end
name = "John"
folder = LuckyTemplate.write!(Path["."]) do |dir|
dir.add_file("README.md", Readme.new("john"))
dir.add_file("Welcome.md") do |io|
io << "# Welcome " << name << "!\n"
end
dir.add_file("LICENSE", <<-LICENSE)
The MIT License (MIT)
...
LICENSE
dir.add_folder("src") do |src|
src.add_file(".keep")
src.add_folder(name.downcase) do |name_dir|
name_dir.add_file("#{name.downcase}.cr", <<-CR)
class #{name}
end
pp! #{name}.new
CR
end
end
end
LuckyTemplate.validate!(Path["."], folder) The above code does the following:
Another way to validate a folder is during testing, using the included Spec helper: john_spec.cr # Let's assume `folder` was still in scope
include LuckyTemplate::Spec
it "template folder for john worked" do
folder.should be_valid_at(Path["."])
end You can even create multiple folders at once, or embed them into other folders, without writing to disk yet: folder1 = LuckyTemplate.create_folder do |dir|
dir.add_file("folder_one.txt")
end
folder2 = LuckyTemplate.create_folder do |dir|
dir.add_file("folder_two.txt")
end
folder3 = LuckyTemplate.create_folder do |dir|
dir.add_file("folder_three.txt")
dir.insert_folder("folder1", folder1)
dir.insert_folder("folder2", folder2)
end
LuckyTemplate.write!(Path["."], folder3) And perhaps the most flexible feature, you can even snapshot folders, for your own use: # Assume above example is still in scope
snapshot = LuckyTemplate.snapshot(folder3)
snapshot.each do |path, type|
puts "All files/folders in Snapshot:"
puts "#{path} => #{type}"
end
snapshot.each do |path, type|
puts "Only files in Snapshot:"
puts "#{path} => #{type}" if type.file?
end
snapshot.each do |path, type|
puts "Only folders in Snapshot:"
puts "#{path} => #{type}" if type.folder?
end Windows support?Yep! LuckyTemplate works on Windows! Examples are cool in theory, but what about actual real-world use?I have a PR up in Lucky's Carbon, that replaces Teeplate with LuckyTemplate, while also improving some stuff that originally needed to be by hand, but can now be done with LuckyTemplate's snapshot feature. It also includes using the Spec helper for validation. Feel free to take a look! Docs?Yes, I've been deploying my docs here. Final wordI know this was long, and tbh, I probably forgot some things I wanted to address. But hopefully, this is a good demonstration of LuckyTemplate and what I've been working on. Let me know what you think! EditOne thing I was thinking I could add here that might be useful to some, is showing what would happen if you didn't use a Crystal implementation for making templates: external_template
external_file.cr class ExternalTemplate
include LuckyTemplate::Fileable
def initialize(@name : String)
end
def to_file(io : IO) : Nil
File.open("#{__DIR__}/external_template") do |input|
Process.run(
command: "envsubst",
input: input,
output: io,
env: {
"NAME" => @name,
}
)
end
end
end
LuckyTemplate.write!(Path["."]) do |dir|
dir.add_file("external_file", ExternalTemplate.new("John"))
end As you can see above, I'm creating a running process using the gettext tool |
Beta Was this translation helpful? Give feedback.
-
lucky_template has been transferred into LuckyFramework and all future work will take place there. |
Beta Was this translation helpful? Give feedback.
UPDATE
I have been working on a POC (proof-of-concept) that implements some of the ideas in my initial suggestion in the discussion, and I'm ready to share what I've created.
Behold...
LuckyTemplate
What is LuckyTemplate?
LuckyTemplate is very similar to Teeplate in its' goals, however, it's much simpler and a lot more capable.
Like Teeplate, you can create a file/folder structure templatized, but instead of using the filesystem, you do it in code.
Here are some of the major points: