Skip to content

Commit

Permalink
project bootstrap (#13)
Browse files Browse the repository at this point in the history
* init codegen and runtime

* updated sbt-plugin name

* update build settings

* sonatype pub

* formatting updates

* add github action sbt plugin

* trying to fix build

* remove publish

* drop scala 2.12 runtime support

* updated readme [ci skip]
  • Loading branch information
touchdown authored Dec 27, 2023
1 parent bd382fc commit 1aac53e
Show file tree
Hide file tree
Showing 18 changed files with 648 additions and 1 deletion.
7 changes: 7 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8

[*.conf]
indent_size = 2
47 changes: 47 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# This file was automatically generated by sbt-github-actions using the
# githubWorkflowGenerate task. You should add and commit this file to
# your git repository. It goes without saying that you shouldn't edit
# this file by hand! Instead, if you wish to make changes, you should
# change your sbt build configuration to revise the workflow description
# to meet your needs, then regenerate this file.

name: Continuous Integration

on:
pull_request:
branches: ['**']
push:
branches: ['**']
tags: [v*]

env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

jobs:
build:
name: Build and Test
strategy:
matrix:
os: [ubuntu-latest]
scala: [2.13.12]
java: [temurin@17]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout current branch (full)
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Java (temurin@17)
if: matrix.java == 'temurin@17'
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
cache: sbt

- name: Check that workflows are up to date
run: sbt '++ ${{ matrix.scala }}' githubWorkflowCheck

- name: Build project
run: sbt '++ ${{ matrix.scala }}' test
60 changes: 60 additions & 0 deletions .github/workflows/clean.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# This file was automatically generated by sbt-github-actions using the
# githubWorkflowGenerate task. You should add and commit this file to
# your git repository. It goes without saying that you shouldn't edit
# this file by hand! Instead, if you wish to make changes, you should
# change your sbt build configuration to revise the workflow description
# to meet your needs, then regenerate this file.

name: Clean

on: push

jobs:
delete-artifacts:
name: Delete Artifacts
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Delete artifacts
shell: bash {0}
run: |
# Customize those three lines with your repository and credentials:
REPO=${GITHUB_API_URL}/repos/${{ github.repository }}
# A shortcut to call GitHub API.
ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; }
# A temporary file which receives HTTP response headers.
TMPFILE=$(mktemp)
# An associative array, key: artifact name, value: number of artifacts of that name.
declare -A ARTCOUNT
# Process all artifacts on this repository, loop on returned "pages".
URL=$REPO/actions/artifacts
while [[ -n "$URL" ]]; do
# Get current page, get response headers in a temporary file.
JSON=$(ghapi --dump-header $TMPFILE "$URL")
# Get URL of next page. Will be empty if we are at the last page.
URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*<//' -e 's/>.*//')
rm -f $TMPFILE
# Number of artifacts on this page:
COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') ))
# Loop on all artifacts on this page.
for ((i=0; $i < $COUNT; i++)); do
# Get name of artifact and count instances of this name.
name=$(jq <<<$JSON -r ".artifacts[$i].name?")
ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1))
id=$(jq <<<$JSON -r ".artifacts[$i].id?")
size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") ))
printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size
ghapi -X DELETE $REPO/actions/artifacts/$id
done
done
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
*.class
*.log

# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*

.bsp
.idea

target/
9 changes: 9 additions & 0 deletions .scalafix.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
rules = [
NoAutoTupling
RemoveUnused
DisableSyntax

LeakingImplicitClassVal
NoValInForComprehension
ProcedureSyntax
]
10 changes: 10 additions & 0 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version = "3.4.3"
runner.dialect = scala213
preset = IntelliJ
maxColumn = 120
docstrings.style = SpaceAsterisk
docstrings.wrap = "no"
newlines.beforeCurlyLambdaParams = multiline
rewrite.rules = [Imports]
rewrite.imports.sort = scalastyle
rewrite.imports.groups = [["sbt\\..*"], ["java\\..*", "javax\\..*"], ["scala\\..*"]]
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,47 @@
# giremock
# gyremock
grpc wrapper for wiremock

## Intro
This project aims to introduce a translation layer between grpc and json (wiremock).
To do so, it relies on the `codegen` logic in the plugin to
1. have access to proto files
2. generate said a translation layer for grpc <-> json
3. generate an aggregate of all the services generated above

Then, the `runtime` library provides the support needed to run the code generated above.

Lastly, the end project still need to write a few lines of code to actually run the service to make it useful and provide all the needed wiremock files.


## Getting Started
in your project's `plugins.sbt`
```sbt
addSbtPlugin("io.github.touchdown" % "sbt-gyremock" % "{version}")
```
then add these lines in your `build.sbt`
```sbt
import gyremock.gen.scaladsl._

libraryDependencies += "io.github.touchdown" %% "gyremock-runtime" % "{version}"
Compile / akkaGrpcExtraGenerators := Seq(TranslatorCodeGen, ServicesBuilderCodeGen)
```
along with your usual protoc settings


finally, add a file like this to bootstrap the actual service
```scala
package xyz

import akka.actor.ActorSystem
import io.github.touchdown.gyremock._

object GyreMockApp {

def main(args: Array[String]): Unit = {
implicit val system: ActorSystem = ActorSystem("GyreMockApp")
val settings = GyremockSettings(system.settings.config.getConfig("gyremock"))
new GyremockServer(settings, ServicesBuilder.build).run()
}

}
```
74 changes: 74 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import sbt.Keys.scalaVersion
import sbt.addSbtPlugin

ThisBuild / organization := "io.github.touchdown"

ThisBuild / dynverVTagPrefix := true
ThisBuild / dynverSeparator := "-"
// append -SNAPSHOT to version when isSnapshot
ThisBuild / dynverSonatypeSnapshots := true
ThisBuild / versionScheme := Some("early-semver")

ThisBuild / sonatypeCredentialHost := "s01.oss.sonatype.org"
ThisBuild / publishTo := sonatypePublishToBundle.value

// enable scalafix
ThisBuild / semanticdbEnabled := true
ThisBuild / semanticdbVersion := scalafixSemanticdb.revision

ThisBuild / scalacOptions ++= Seq("-Ywarn-unused")

ThisBuild / githubWorkflowJavaVersions := Seq(JavaSpec.temurin("17"))
ThisBuild / crossScalaVersions := Dependencies.Versions.CrossScalaForLib
ThisBuild / githubWorkflowTargetTags ++= Seq("v*")

ThisBuild / githubWorkflowPublishTargetBranches := Seq()

val gyremockRuntimeName = "gyremock-runtime"
val akkaGrpcVersion = "2.1.6"

lazy val codegen = Project(id = "gyremock-codegen", base = file("codegen"))
.settings(resolvers += Resolver.sbtPluginRepo("releases"))
.enablePlugins(BuildInfoPlugin)
.settings(
scalaVersion := Dependencies.Versions.CrossScalaForPlugin.head,
addSbtPlugin("com.lightbend.akka.grpc" % "sbt-akka-grpc" % akkaGrpcVersion),
buildInfoKeys ++= Seq[BuildInfoKey](organization, name, version, scalaVersion, sbtVersion),
buildInfoKeys += "runtimeArtifactName" -> gyremockRuntimeName,
buildInfoPackage := "gyremock.gen"
)

lazy val sbtPlugin = Project(id = "sbt-gyremock", base = file("sbt-plugin"))
.enablePlugins(SbtPlugin)
.settings(
crossScalaVersions := Dependencies.Versions.CrossScalaForPlugin,
scalaVersion := Dependencies.Versions.CrossScalaForPlugin.head
)
.dependsOn(codegen)

lazy val runtime = Project(id = gyremockRuntimeName, base = file("runtime"))
.settings(
crossScalaVersions := Dependencies.Versions.CrossScalaForLib,
scalaVersion := Dependencies.Versions.CrossScalaForLib.head,
libraryDependencies := Seq(
"ch.qos.logback" % "logback-classic" % "1.4.14" % Runtime,
"com.lightbend.akka.grpc" %% "akka-grpc-runtime" % akkaGrpcVersion,
"com.typesafe.akka" %% "akka-actor" % "2.6.20",
"com.typesafe.akka" %% "akka-http-core" % "10.2.9",
"com.thesamet.scalapb" %% "scalapb-json4s" % "0.11.1",
"com.typesafe.scala-logging" %% "scala-logging" % "3.9.2",
"org.scalatest" %% "scalatest" % "3.1.0" % Test
)
)

lazy val root = Project(id = "gyremock", base = file("."))
.aggregate(runtime, codegen, sbtPlugin)
.settings(
publish / skip := true,
// https://github.com/sbt/sbt/issues/3465
// Libs and plugins must share a version. The root project must use that
// version (and set the crossScalaVersions as empty list) so each sub-project
// can then decide which scalaVersion and crossCalaVersions they use.
crossScalaVersions := Nil,
scalaVersion := Dependencies.Versions.scala212
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package gyremock.gen.scaladsl

import scala.collection.immutable
import scala.jdk.CollectionConverters._

import akka.grpc.gen.Logger
import akka.grpc.gen.scaladsl.{ScalaCodeGenerator, Service}
import com.google.protobuf.compiler.PluginProtos.{CodeGeneratorRequest, CodeGeneratorResponse}
import scalapb.compiler._

/** this generates the code to bootstrap the grpc-json translation service
*/
object ServicesBuilderCodeGen extends ScalaCodeGenerator {
override val name: String = "services-builder"

override def run(request: CodeGeneratorRequest, logger: Logger): CodeGeneratorResponse = {
logger.info("Generating services builder")
val b = CodeGeneratorResponse.File.newBuilder()
val services = getServices(request)
b.setContent(new ObjectCodeGenerator(services).run())
b.setName("io/github/touchdown/gyremock/ServicesBuilder.scala")
CodeGeneratorResponse
.newBuilder()
.setSupportedFeatures(CodeGeneratorResponse.Feature.FEATURE_PROTO3_OPTIONAL.getNumber)
.addFile(b)
.build()
}

// code is copied and pasted from https://github.com/akka/akka-grpc/blob/master/codegen/src/main/scala/akka/grpc/gen/scaladsl/ScalaCodeGenerator.scala
// will remove once its been fixed to be a protected method upstream
private def getServices(request: CodeGeneratorRequest) = {
// Currently per-invocation options, intended to become per-service options eventually
// https://github.com/akka/akka-grpc/issues/451
val params = request.getParameter.toLowerCase
// flags listed in akkaGrpcCodeGeneratorSettings's description
val serverPowerApi = params.contains("server_power_apis") && !params.contains("server_power_apis=false")
val usePlayActions = params.contains("use_play_actions") && !params.contains("use_play_actions=false")

val codeGenRequest = protocgen.CodeGenRequest(request)
(for {
fileDesc <- codeGenRequest.filesToGenerate
serviceDesc <- fileDesc.getServices.asScala
} yield Service(
codeGenRequest,
parseParameters(request.getParameter),
fileDesc,
serviceDesc,
serverPowerApi,
usePlayActions
)).toList
}

// flags listed in akkaGrpcCodeGeneratorSettings's description
private def parseParameters(params: String): GeneratorParams =
params.split(",").map(_.trim).filter(_.nonEmpty).foldLeft[GeneratorParams](GeneratorParams()) {
case (p, "java_conversions") => p.copy(javaConversions = true)
case (p, "flat_package") => p.copy(flatPackage = true)
case (p, "single_line_to_string") => p.copy(singleLineToProtoString = true) // for backward-compatibility
case (p, "single_line_to_proto_string") => p.copy(singleLineToProtoString = true)
case (p, "ascii_format_to_string") => p.copy(asciiFormatToString = true)
case (p, "no_lenses") => p.copy(lenses = false)
case (p, "retain_source_code_info") => p.copy(retainSourceCodeInfo = true)
case (p, "grpc") => p.copy(grpc = true)
case (x, _) => x
}
}

final private class ObjectCodeGenerator(services: immutable.Seq[Service]) {

def run(): String = {
new FunctionalPrinter()
.add("// Generated by the Scala Gyremock Plugin for the Protocol Buffer Compiler")
.add("// Do not edit!")
.newline
.call(addPackageClause)
.newline
.call(addImportStatements)
.newline
.call(addObject)
.newline
.result
}

private def addPackageClause(printer: FunctionalPrinter): FunctionalPrinter = {
printer.add("package io.github.touchdown.gyremock")
}

private def addImportStatements(printer: FunctionalPrinter): FunctionalPrinter = {
val libraryImports = Seq("akka.actor.ActorSystem", "scala.collection.immutable")

printer.add(libraryImports.map(i => s"import $i"): _*)
}

private def addObject(printer: FunctionalPrinter): FunctionalPrinter = {
printer
.add("object ServicesBuilder {")
.newline
.addIndented("def build(implicit sys: ActorSystem): immutable.Seq[Service] = List(")
.indent
.print(services) {
(p, s) =>
p.addIndented(
s"Service(${s.packageName}.${s.name}, fromTo => ${s.packageName}.${s.name}Handler.partial(new ${s.packageName}.${s.name}Translate(fromTo))),"
)
}
.add(")")
.outdent
.add("}")
.newline
}
}
Loading

0 comments on commit 1aac53e

Please sign in to comment.