Created
June 5, 2026 09:54
-
-
Save Yummy-Yums/657383fa9226455ebfe6ff2954b903cb to your computer and use it in GitHub Desktop.
Code to generate Google Cloud Events based on the json schemas . run in a typelevel project
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import os._ | |
| import io.circe._, io.circe.parser._ | |
| import scala.meta._ | |
| import org.scalafmt.Scalafmt._ | |
| import org.scalafmt.config.ScalafmtConfig | |
| /* | |
| - parse the events json. take note of inner objects | |
| take the "defintions" key/value pair check if there's a "$ref" in definitions, | |
| and parse it into json separately. | |
| extract the values with the cursor into a list of key/value pairs, traverse though the types | |
| and do a toType conversion is possible , check the returnTypes | |
| - define imports | |
| - define makeAbstrac tClass | |
| - define makeApply | |
| - define makeImpl | |
| */ | |
| object GoogleCloudEvents extends App { | |
| val msg_published = Utils.parse_json_and_return_cursor("message_published.json") | |
| val cursor = msg_published.hcursor | |
| val definitions = cursor.downField("definitions") | |
| val keysOfDefintions = definitions.keys.get.toList | |
| val fileName = keysOfDefintions.head | |
| val valuesOfDefintions = keysOfDefintions.map{ key => | |
| key -> definitions.downField(key).as[Json] | |
| } | |
| val wd = os.pwd / "src" / "main" / "scala" | |
| var propsAndTypesOfValuesDefns = List.empty[List[(String, Type)]] | |
| valuesOfDefintions.foreach { | |
| case (name, valuePairs) => { | |
| val properties = valuePairs.toOption.get | |
| propsAndTypesOfValuesDefns = | |
| propsAndTypesOfValuesDefns :+ Utils.getPropertiesAndTypes(properties) | |
| } | |
| } | |
| if (!os.exists(wd / s"${fileName}.scala")){ | |
| println(s"creating file: ${fileName}.scala") | |
| os.write(wd / s"${fileName}.scala", "") | |
| } | |
| var map = Map.empty[String, List[Decl.Def]] | |
| for ( | |
| (key, defn) <- keysOfDefintions.zip(propsAndTypesOfValuesDefns) | |
| ) yield { | |
| val res = defn.map(elem => { | |
| val (propertyName, propertyType) = elem | |
| Utils.makeMethod(propertyName, propertyType) | |
| }) | |
| map = map + (key -> res) | |
| } | |
| val codeGen = map.map{ | |
| case(className, methods) => { | |
| val abstractClass = Utils.makeAbstractClassWithDefMethods(className, methods) | |
| val (applyMethod, decoder, implCaseClass, objectTermName) = Utils.makeApplyAndImpl(className, methods) | |
| // (applyMethod, decoder, implCaseClass, objectNameTerm) | |
| source""" | |
| $abstractClass | |
| object $objectTermName { | |
| $applyMethod | |
| $decoder | |
| $implCaseClass | |
| } | |
| """ | |
| }} | |
| val source = source""" | |
| ..${Utils.imports} | |
| ..${codeGen.flatMap(_.stats).toList} | |
| """ | |
| val formatted = format(source.syntax, ScalafmtConfig.default).get | |
| println(formatted) | |
| os.write.over(wd / s"${fileName}.scala", formatted) | |
| } | |
| object Utils { | |
| def parse_json_and_return_cursor(file_name: String) = { | |
| val wd = os.pwd / "src" / "main" / "scala" | |
| val read = os.read(wd / file_name) | |
| val parsed_json = parse(read) | |
| parsed_json match { | |
| case Left(err) => println(s"Error parsing JSON: $err"); Json.Null | |
| case Right(json) => json | |
| } | |
| } | |
| // add newer imports here | |
| val imports = List( | |
| q"import io.circe._", | |
| q"import io.circe.Decoder" | |
| ) | |
| def makeMethod( | |
| propertyName: String, | |
| propertyType: Type | |
| ) = { | |
| val methodName = Term.Name(propertyName) | |
| // val typeParam = Type.Name(propertyType) | |
| q"def ${methodName}: $propertyType" | |
| } | |
| def makeAbstractClass( | |
| className: String, | |
| methods: List[(String, String, String)] | |
| ) = { | |
| val methods_ = methods.map { | |
| case (methodName, returnType, "") => { | |
| val methodName_ = Term.Name(methodName) | |
| val typeParams = Type.Name(returnType) | |
| q"def ${methodName_}: $typeParams" | |
| } | |
| case (methodName, returnType, typeArg) => { | |
| val methodName_ = Term.Name(methodName) | |
| val typeParams = Type.Apply( | |
| Type.Name(returnType), | |
| Type.ArgClause(List(Type.Name(typeArg))) | |
| ) | |
| q"def ${methodName_}: $typeParams" | |
| } | |
| } | |
| q""" | |
| sealed abstract class ${Type.Name(className)} { | |
| ..${methods_} | |
| } | |
| """ | |
| } | |
| def makeAbstractClassWithDefMethods( | |
| className: String, | |
| methods: List[Decl.Def] | |
| ) = { | |
| val name = Type.Name(className) | |
| q""" | |
| sealed abstract class $name { | |
| ..$methods | |
| } | |
| """ | |
| } | |
| def makeApplyAndImpl( | |
| objectName: String, | |
| methods: List[Decl.Def] | |
| ) = { | |
| val returnType = Type.Name(objectName) | |
| val impl = Type.Name("Impl") | |
| val productPrefix = Lit.String(objectName) | |
| val caseClassExtendsClass = Init(returnType, Name("Impl"), List.empty) | |
| val params = methods.map{ method => | |
| val paramName = method.name | |
| val paramType = method.decltpe | |
| (paramName, paramType) | |
| } | |
| val params_apply = params.map { | |
| case (paramName, paramType) => { | |
| param"$paramName: $paramType" | |
| } | |
| } | |
| val applyMethod = q""" | |
| def apply( | |
| ..$params_apply | |
| ): $returnType = { | |
| new $impl(..${params.map { | |
| case (key, _) => key | |
| }}) | |
| } | |
| """ | |
| val implCaseClass = q""" | |
| private final case class $impl( | |
| ..$params_apply | |
| ) extends $caseClassExtendsClass { | |
| override def productPrefix = $productPrefix | |
| } | |
| """ | |
| val forProductN = Term.Name(s"forProduct${params.size}") | |
| val objectNameTerm = Term.Name(objectName) | |
| val decoder = q""" | |
| implicit val decoder: Decoder[$returnType] = | |
| Decoder.$forProductN( | |
| ..${params.map { case (paramName, _) => Lit.String(paramName.value) }} | |
| )($objectNameTerm.apply) | |
| """ | |
| (applyMethod, decoder, implCaseClass, objectNameTerm) | |
| } | |
| def getPropertiesAndTypes(properties: Json): List[(String, Type)] = { | |
| // get the cursor, get the keys, map over them , if the values of properties, | |
| // contain $ref , split the values by '/' and take the last value | |
| // other than just get the (propertyName -> propertyType) | |
| var result = List.empty[(String, Type)] | |
| val cursor = properties.hcursor | |
| val keysOfProperties = cursor.keys.get.toList | |
| keysOfProperties.map { currentKeyOfProperties => | |
| if (currentKeyOfProperties == "properties"){ | |
| val valueCursor = cursor.downField("properties").as[Json].toOption.get.hcursor | |
| val keysInValues = valueCursor.keys.get.toList.map { key => | |
| val keyValueFields = valueCursor.downField(key).as[Json] | |
| keyValueFields.map { pairs => | |
| val pairCursor = pairs.hcursor | |
| pairCursor.keys.get.foreach { | |
| case "$ref" => { | |
| val ref = pairCursor.downField("$ref").as[String] | |
| .toOption | |
| .get | |
| .split('/') | |
| .last | |
| result = result :+ (key -> Type.Name(ref)) | |
| } | |
| case "type" => { | |
| val type_ = pairCursor.downField("type").as[String].toOption.get | |
| if (type_ == "object"){ | |
| val additionalProperties = pairCursor | |
| .downField("additionalProperties").as[Json] | |
| .toOption | |
| .get | |
| .hcursor | |
| .downField("type").as[String].toOption.get | |
| result = result :+ (key -> convertType(type_, additionalProperties)) | |
| } else { | |
| result = result :+ (key -> convertType(type_)) | |
| } | |
| } | |
| case _ => () | |
| } | |
| } | |
| } | |
| } else { | |
| val value = cursor.downField(currentKeyOfProperties).as[String] | |
| } | |
| } | |
| result | |
| } | |
| def convertType(property: String, additionalProperties: String = ""): Type ={ | |
| property match { | |
| case "string" => Type.Name("String") | |
| case "integer" => Type.Name("Int") | |
| case "boolean" => Type.Name("Boolean") | |
| case "number" => Type.Name("Double") | |
| case "array" => Type.Apply(Type.Name("List"), Type.ArgClause(List(Type.Name("String")))) | |
| case "object" => | |
| if (additionalProperties.nonEmpty) | |
| Type.Apply( | |
| Type.Name("Map"), | |
| Type.ArgClause(List(Type.Name("String"), convertType(additionalProperties))) | |
| ) | |
| else | |
| Type.Apply(Type.Name("Map"), Type.ArgClause(List(Type.Name("String"), Type.Name("String")))) | |
| case _ => Type.Name("String") | |
| } | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
i'm using this piece of code to generate the google cloud events using the definitions key in each schema