yurique / embedded-files   0.4.0

MIT License GitHub

An sbt plugin to generate Scala objects containing the contents of glob-specified files as strings or byte-arrays.

Scala versions: 2.13 2.12
Scala.js versions: 1.x
sbt plugins: 1.0

embedded-files

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.

Installation

plugins.sbt

addSbtPlugin("com.yurique" % "sbt-embedded-files" % "0.4.0")

build.sbt

 libraryDependencies += "com.yurique" %%% "embedded-files-macro" % "{embedded-files-macro-version}"

Example usage

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."

Configuration

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
    )

Generated files

Interfaces

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]
}

Text files

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"""

}

Binary files

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
  )

}

Package- and class-names

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 to Compile / 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

Index file

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
  )
}

Transforming text

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.

macros

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.

Missing embedded files

The macros are not currently doing any checks for whether the embedded files exist. If they don't, scalac will just fail to compile.