An sbt plugin to generate Scala objects containing the contents of glob-specified files as strings or byte-arrays.
The accompanying macro allowing to access those objects more easily: embedded-files-macro.
addSbtPlugin("com.yurique" % "sbt-embedded-files" % "0.4.0")
libraryDependencies += "com.yurique" %%% "embedded-files-macro" % "{embedded-files-macro-version}"
Put a file into src/main/resources/docs/test.txt
:
I'm a test text.
Add embedFiles
to the Compile / sourceGenerators
:
project
// ...
.enablePlugins(EmbeddedFilesPlugin)
// ...
.settings(
(Compile / sourceGenerators) += embedFiles
)
In the code:
import com.yurique.embedded.FileAsString
val testTxtContents = FileAsString("/docs/test.txt") // "I'm a test text."
The sbt plugin has the following configuration keys:
project
// ...
.settings(
// default is __embedded_files, which is assumed by the macro
// the generated objects and classes are put into this package (and sub-packages)
embedRootPackage := "custom_root_package",
// a list of directories to look for files to embed in
// default is (Compile / unmanagedResourceDirectories)
embedDirectories ++= (Compile / unmanagedSourceDirectories).value,
// a list of globs for text files
// default is Seq("**/*.txt")
embedTextGlobs := Seq("**/*.txt", "**/*.md"),
// a list of globs for binary files
// default is Seq.empty
embedBinGlobs := Seq("**/*.bin"),
// whether or not to generate a EmbeddedFilesIndex object containing references to all embedded files
// default is false
embedGenerateIndex := true,
// the intended usage is to use the output of the embedFiles task as generated sources
(Compile / sourceGenerators) += embedFiles
)
The embedFiles
always generates these two abstract classes:
package ${embedRootPackage.value}
abstract class EmbeddedTextFile {
def path: String
def content: String
}
and
package ${embedRootPackage.value}
abstract class EmbeddedBinFile {
def path: String
def content: Array[Byte]
}
For each file in the embedDirectories
that matches any of the embedTextGlobs
a file like the following is generated:
package ${embedRootPackage.value}.${subPackage}
object ${className} extends __embedded_files.EmbeddedTextFile {
val path: String = """path/to/the/file/filename.txt"""
val content: String = """the content of the file"""
}
For each file in the embedDirectories
that matches any of the embedBinGlobs
a file like the following is generated:
package ${embedRootPackage.value}.${subPackage}
object ${className} extends __embedded_files.EmbeddedBinFile {
val path: String = """path/to/the/file/filename.bin"""
val content: Array[Byte] = Array(
// example bytes
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f
)
}
For each file, its path is taken relative to the one of the embedDirectories
and converted into the package name and the class name.
For example, if the file was /home/user/.../.../project/src/main/resources/some-dir/1 some sub dir/some things & other things.txt
:
- a relative path is
some-dir/1 some sub dir/some things & other things.txt
(relative toCompile / unmanagedSourceDirectories
in this example) - the dirname is
some-dir/1 some sub dir
, it is split by/
, every part is converted to a valid Scala ID (by replacing non alpha-numerics by_
and prepending_
is the path starts with a digit) - the resulting package name is
some_dir._1_some_sub_dir
- the class name is derived from the file name:
some_things_other_things
if embedGenerateIndex
is set to true
, the index file is generated like the following:
package ${embedRootPackage.value}
object EmbeddedFilesIndex {
val textFiles: Seq[(String, EmbeddedTextFile)] = Seq(
"test/test-resource.txt" -> __embedded_files.test.test_resource_txt,
"com/company/test_file_1.txt" -> __embedded_files.com.company.test_file_1_txt
)
val binFiles: Seq[(String, EmbeddedBinFile)] = Seq(
"test/test-bin-resource.bin" -> __embedded_files.test.test_bin_resource_bin,
"com/company/test_bin_file_1.bin" -> __embedded_files.com.company.test_bin_file_1_bin
)
}
It is possible to configure a basic String => String
transformation for text files.
project
// ...
.settings(
embedDirectories ++= (Compile / unmanagedSourceDirectories).value,
embedTextGlobs := Seq("**/*.md"),
embedTransform := Seq(
TransformConfig(
when = _.getFileName.toString.endsWith(".md"),
transform = _.toUpperCase
)
),
(Compile / sourceGenerators) += embedFiles
)
For each text file, the .transform
function of the first of the TransformConfig
-s that matches the path (.when
returns true
)
will be applied to the contents of the file.
If none of the configuration matches - the file contents will be used as is.
If more than one configuration matches, only the first one will be used.
Assuming the embedFiles
is used as a source generator with the default root package, you can use the macros provided by the embedded-files-macro
library to get the string/byte-array content of the files like this:
import com.yurique.embedded._
val s: String = FileAsString("/docs/test.txt")
val b: Array[Byte] = FileAsByteArray("/bins/test.bin")
If the path passed to the macros starts with a slash, it is used as is.
If it doesn't start with a slash, the macro does the following:
/home/user/.../project/src/main/scala/com/company/MyClass.scala
package com.company.MyClass
object MyClass {
val s: String = FileAsString("dir/data.txt")
}
Here, the file name doesn't start with a /
– dir/data.txt
.
The calling site is in the /home/user/.../project/src/main/scala/com/company/
directory.
The requested file name is appended to this directory, resulting in /home/user/.../project/src/main/scala/com/company/dir/data.txt
.
This file is taken relative to the first scala
directory in the path, resulting in /com/company/dir/data.txt
.
The macros are not currently doing any checks for whether the embedded files exist. If they don't, scalac
will just fail to compile.