Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
yurique committed Jan 2, 2021
0 parents commit 0e4ae6d
Show file tree
Hide file tree
Showing 49 changed files with 1,046 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.vscode
.metals
.idea
.DS_Store
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Changelog

## 0.1.0

Initial release.
9 changes: 9 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# The MIT License (MIT)

Copyright 2020 Iurii Malchenko

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
208 changes: 208 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
![Maven Central](https://img.shields.io/maven-central/v/com.yurique/embedded-files-macro_sjs1_2.13.svg)

# embedded-files

An sbt plugin to create Scala objects containing the contents of the files as `String`s or `Array[Byte]`s.

And an acompanying macro to more easily access those objects.

## Installation

### `plugins.sbt`

```scala
addSbtPlugin("com.yurique" % "sbt-embedded-files" % "0.1.0")
```

### `build.sbt`

```scala
libraryDependencies += "com.yurique" %%% "embedded-files-macro" % "0.1.0"
```

## Example usage

Put a file into `src/main/resources/docs/test.txt`:

```
I'm a test text.
```

Add `embedFiles` to the `Compile / sourceGenerators`:

```scala
project
// ...
.settings(
(Compile / sourceGenerators) += embedFiles
)
```

In the code:

```scala
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:

```scala
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:

```scala
package ${embedRootPackage.value}

abstract class EmbeddedTextFile {
def path: String
def content: String
}

```

and

```scala
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:

```scala
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:

```scala
package ${embedRootPackage.value}.${subPackage}

object ${className} extends __embedded_files.EmbeddedBinFile {

val path: String = """path/to/the/file/filename.txt"""

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:

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

## 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:

```scala
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`
```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.

Those requested file 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, the scalac will just fail to compile in a normal way.

4 changes: 4 additions & 0 deletions embedded-files-macro/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
target
.idea
.bsp

18 changes: 18 additions & 0 deletions embedded-files-macro/.scalafmt.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
version=2.7.5
style = defaultWithAlign
align.openParenCallSite = true
align.openParenDefnSite = true
align.arrowEnumeratorGenerator = true
maxColumn = 180
continuationIndent.defnSite = 2
assumeStandardLibraryStripMargin = true
danglingParentheses.defnSite = true
danglingParentheses.callSite = true
rewrite.rules = [AvoidInfix, ExpandImportSelectors, RedundantParens, SortModifiers]
docstrings = JavaDoc
newlines.afterCurlyLambda = preserve
docstrings.style = Asterisk
docstrings.oneline = unfold
trailingCommas = "never"
optIn.breaksInsideChains = true
includeCurlyBraceInSelectChains = true
52 changes: 52 additions & 0 deletions embedded-files-macro/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
ThisBuild / organization := "com.yurique"
ThisBuild / homepage := Some(url("https://github.com/yurique/embedded-files"))
ThisBuild / licenses += ("MIT", url("https://github.com/yurique/embedded-files/blob/main/LICENSE.md"))
ThisBuild / developers := List(
Developer(
id = "yurique",
name = "Iurii Malchenko",
email = "[email protected]",
url = url("https://github.com/yurique")
)
)
ThisBuild / scmInfo := Some(
ScmInfo(
url("https://github.com/yurique/embedded-files"),
"scm:[email protected]/yurique/embedded-files.git"
)
)
ThisBuild / releasePublishArtifactsAction := PgpKeys.publishSigned.value
ThisBuild / publishTo := sonatypePublishToBundle.value
ThisBuild / pomIncludeRepository := { _ => false }
ThisBuild / sonatypeProfileName := "yurique"
ThisBuild / publishArtifact in Test := false
ThisBuild / publishMavenStyle := true
ThisBuild / releaseCrossBuild := true

ThisBuild / scalaVersion := "2.13.4"
ThisBuild / crossScalaVersions := Seq("2.12.12", "2.13.4")

lazy val noPublish = Seq(
publishLocal / skip := true,
publish / skip := true,
publishTo := Some(Resolver.file("Unused transient repository", file("target/unusedrepo")))
)

lazy val `embedded-files-macro` = crossProject(JVMPlatform, JSPlatform)
.crossType(CrossType.Pure)
.in(file("macro"))
.enablePlugins(ScalaJSPlugin)
.settings(
libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value
)

lazy val root = project
.in(file("."))
.settings(
name := "embedded-files"
)
.settings(noPublish)
.aggregate(
`embedded-files-macro`.jvm,
`embedded-files-macro`.js
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.yurique.embedded

import java.io.File
import scala.reflect.macros.blackbox

object FileAsByteArray {

var rootPackage: String = "__embedded_files"
def apply(fileName: String): Array[Byte] = macro FileAsByteArrayImpl.referEmbeddedFile

}

class FileAsByteArrayImpl(val c: blackbox.Context) {
import c._
import universe._

def referEmbeddedFile(fileName: c.Expr[String]) = fileName.tree match {
case Literal(Constant(fileNameStr: String)) =>
val file = {
if (fileNameStr.startsWith("/")) {
new File(fileNameStr)
} else {
new File(new File(c.enclosingPosition.source.path).getParentFile.getAbsolutePath.split('/').dropWhile(_ != "scala").drop(1).mkString("/"), fileNameStr)
}
}.toPath

val packageName: String = FileAsByteArray.rootPackage + "." + file.getParent.toString.replace("/", ".").dropWhile(_ == '.')

val className: String =
s"${file.getFileName.toString.replaceAll("\\W", "_").replaceAll("_+", "_")}"

val maybeSelectObject = packageName
.split('.').foldLeft[Option[c.Tree]](
None
) { (chain, next) =>
chain match {
case None => Some(Ident(TermName(next)))
case Some(chain) => Some(Select(chain, TermName(next)))
}
}

maybeSelectObject
.map { selectObject =>
Select(Select(selectObject, TermName(className)), TermName("content"))
}.getOrElse(throw new RuntimeException(s"invalid package and class: ${packageName} ${className} for file ${fileNameStr}"))
}

}
Loading

0 comments on commit 0e4ae6d

Please sign in to comment.