diff --git a/.gitignore b/.gitignore index d84304bb..1a18426e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ generated-*/ # Temporary test directories for parallel test execution target/test-regular/ target/test-wrapped/ +target/test-graphql/ src/test/resources/internal diff --git a/.mdl/defs/tests.md b/.mdl/defs/tests.md index e0f20d2d..60653c0e 100644 --- a/.mdl/defs/tests.md +++ b/.mdl/defs/tests.md @@ -1837,12 +1837,88 @@ popd ret success:bool=true ``` +# action: test-gen-graphql + +Generate GraphQL SDL schemas and validate them. + +```bash +BABOON_BIN="${action.build.binary}" +TEST_DIR="./target/test-graphql" + +rm -rf "$TEST_DIR" +mkdir -p "$TEST_DIR" + +$BABOON_BIN \ + --model-dir ./baboon-compiler/src/test/resources/baboon/ \ + :graphql \ + --output "$TEST_DIR" \ + --disable-conversions=true \ + --runtime=without + +ret success:bool=true +ret test_dir:string="$TEST_DIR" +``` + +# action: test-graphql + +Validate generated GraphQL schemas using graphql-js buildSchema. + +```bash +TEST_DIR="${action.test-gen-graphql.test_dir}" + +pushd ./test/gql-stub +npm install +node validate.mjs "../../$TEST_DIR" +popd + +ret success:bool=true +``` + +# action: test-gen-openapi + +Generate OpenAPI 3.1 component schemas and validate them. + +```bash +BABOON_BIN="${action.build.binary}" +TEST_DIR="./target/test-openapi" + +rm -rf "$TEST_DIR" +mkdir -p "$TEST_DIR" + +$BABOON_BIN \ + --model-dir ./baboon-compiler/src/test/resources/baboon/ \ + :openapi \ + --output "$TEST_DIR" \ + --disable-conversions=true \ + --runtime=without + +ret success:bool=true +ret test_dir:string="$TEST_DIR" +``` + +# action: test-openapi + +Validate generated OpenAPI schemas using swagger-parser. + +```bash +TEST_DIR="${action.test-gen-openapi.test_dir}" + +pushd ./test/oas-stub +npm install +node validate.mjs "../../$TEST_DIR" +popd + +ret success:bool=true +``` + # action: test Run complete test suite (orchestrator action). ```bash dep action.test-sbt-basic +dep action.test-graphql +dep action.test-openapi dep action.test-cs-regular dep action.test-scala-regular dep action.test-python-regular diff --git a/CLAUDE.md b/CLAUDE.md index e10b780f..33cb6b1e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -Baboon is a Domain Modeling Language (DML) compiler with schema evolution support. It compiles `.baboon` domain model files to multiple target languages (Scala, C#, Python, Rust, TypeScript, Kotlin, Java, Dart, Swift) with automatic JSON and UEBA codec generation. +Baboon is a Domain Modeling Language (DML) compiler with schema evolution support. It compiles `.baboon` domain model files to multiple target languages (Scala, C#, Python, Rust, TypeScript, Kotlin, Java, Dart, Swift) with automatic JSON and UEBA codec generation. It also supports schema-only output formats (GraphQL SDL, OpenAPI 3.1). ## Essential Commands @@ -108,6 +108,8 @@ baboon \ - `java/` - Java code generation with Jackson JSON codecs and UEBA binary codecs - `dart/` - Dart code generation with dart:convert JSON codecs and UEBA binary codecs - `swift/` - Swift code generation with JSONSerialization JSON codecs and UEBA binary codecs + - `graphql/` - GraphQL SDL schema generation (type definitions only, no codecs) + - `openapi/` - OpenAPI 3.1 JSON Schema generation (component schemas only, no codecs) - Each generator produces source files, codec implementations and conversions from lower versions to higher ones 5. **Runtime Support (`src/main/resources/baboon-runtime/`)** @@ -146,7 +148,7 @@ Baboon files support: - Unit tests for individual components - Integration tests with full compilation cycles -- Generated code tests in `test/cs-stub/`, `test/sc-stub/`, `test/py-stub/`, `test/rs-stub/`, `test/ts-stub/`, `test/kt-stub/`, `test/jv-stub/`, `test/dt-stub/`, and `test/sw-stub/` +- Generated code tests in `test/cs-stub/`, `test/sc-stub/`, `test/py-stub/`, `test/rs-stub/`, `test/ts-stub/`, `test/kt-stub/`, `test/jv-stub/`, `test/dt-stub/`, `test/sw-stub/`, `test/gql-stub/`, and `test/oas-stub/` - Cross-platform compatibility tests in `test/conv-test-{cs,sc,py,rs,ts,kt,jv,dt,sw}/` (verifies JSON/UEBA interop across all languages) - Evolution tests validating schema migration diff --git a/baboon-compiler/.js/src/main/scala/io/septimalmind/baboon/BaboonJS.scala b/baboon-compiler/.js/src/main/scala/io/septimalmind/baboon/BaboonJS.scala index e2e700c4..ffc7c711 100644 --- a/baboon-compiler/.js/src/main/scala/io/septimalmind/baboon/BaboonJS.scala +++ b/baboon-compiler/.js/src/main/scala/io/septimalmind/baboon/BaboonJS.scala @@ -73,8 +73,7 @@ object BaboonJS { obj.asInstanceOf[JSCompilationError] } - class BaboonCompilationException(val issues: NEList[BaboonIssue]) - extends RuntimeException(s"Compilation failed with ${issues.toList.size} issues") + class BaboonCompilationException(val issues: NEList[BaboonIssue]) extends RuntimeException(s"Compilation failed with ${issues.toList.size} issues") /** * Compilation result @@ -330,7 +329,7 @@ object BaboonJS { private def createOutputOptions(generic: js.UndefOr[JSGenericOptions]): OutputOptionsJS = { val g = generic.getOrElse(js.Dynamic.literal().asInstanceOf[JSGenericOptions]) OutputOptionsJS( - safeToRemoveExtensions = Set("meta", "cs", "json", "scala", "kt", "py", "rs", "ts", "java", "dart", "swift"), + safeToRemoveExtensions = Set("meta", "cs", "json", "scala", "kt", "py", "rs", "ts", "java", "dart", "swift", "graphql"), runtime = parseRuntimeOpt(g.runtime), generateConversions = !g.disableConversions.getOrElse(false), generateTests = g.generateTests.getOrElse(false), @@ -535,6 +534,20 @@ object BaboonJS { pragmas = Map.empty, ), ) + case "graphql" => + CompilerTargetJS.GqlTarget( + id = "GraphQL", + output = createOutputOptions(target.generic), + generic = createGenericOptions(target.generic), + language = GqlOptions(pragmas = Map.empty), + ) + case "openapi" => + CompilerTargetJS.OasTarget( + id = "OpenAPI", + output = createOutputOptions(target.generic), + generic = createGenericOptions(target.generic), + language = OasOptions(pragmas = Map.empty), + ) case other => throw new IllegalArgumentException(s"Unknown target language: $other") } } @@ -652,17 +665,37 @@ object BaboonJS { ) } + private def toGqlTarget(target: CompilerTargetJS.GqlTarget): CompilerTarget.GqlTarget = { + CompilerTarget.GqlTarget( + id = target.id, + output = toOutputOptions(target.output), + generic = target.generic, + language = target.language, + ) + } + + private def toOasTarget(target: CompilerTargetJS.OasTarget): CompilerTarget.OasTarget = { + CompilerTarget.OasTarget( + id = target.id, + output = toOutputOptions(target.output), + generic = target.generic, + language = target.language, + ) + } + private def toCompilerTargets(targets: Seq[CompilerTargetJS]): Seq[CompilerTarget] = { targets.map { - case t: CompilerTargetJS.CSTarget => toCSTarget(t) - case t: CompilerTargetJS.ScTarget => toScTarget(t) - case t: CompilerTargetJS.PyTarget => toPyTarget(t) - case t: CompilerTargetJS.RsTarget => toRsTarget(t) - case t: CompilerTargetJS.TsTarget => toTsTarget(t) - case t: CompilerTargetJS.KtTarget => toKtTarget(t) - case t: CompilerTargetJS.JvTarget => toJvTarget(t) - case t: CompilerTargetJS.DtTarget => toDtTarget(t) - case t: CompilerTargetJS.SwTarget => toSwTarget(t) + case t: CompilerTargetJS.CSTarget => toCSTarget(t) + case t: CompilerTargetJS.ScTarget => toScTarget(t) + case t: CompilerTargetJS.PyTarget => toPyTarget(t) + case t: CompilerTargetJS.RsTarget => toRsTarget(t) + case t: CompilerTargetJS.TsTarget => toTsTarget(t) + case t: CompilerTargetJS.KtTarget => toKtTarget(t) + case t: CompilerTargetJS.JvTarget => toJvTarget(t) + case t: CompilerTargetJS.DtTarget => toDtTarget(t) + case t: CompilerTargetJS.SwTarget => toSwTarget(t) + case t: CompilerTargetJS.GqlTarget => toGqlTarget(t) + case t: CompilerTargetJS.OasTarget => toOasTarget(t) } } @@ -740,25 +773,29 @@ object BaboonJS { } // skip ADT branch members (they're owned by their parent ADT) if (!user.ownedByAdt) { - result.push(JSTypeInfo( - pkg = pkg.toString, - version = version.toString, - id = typeId.toString, - name = user.defn.id.name.name, - kind = kind, - )) + result.push( + JSTypeInfo( + pkg = pkg.toString, + version = version.toString, + id = typeId.toString, + name = user.defn.id.name.name, + kind = kind, + ) + ) } case _ => // skip builtins } } for (alias <- domain.aliases) { - result.push(JSTypeInfo( - pkg = pkg.toString, - version = version.toString, - id = s"type:${alias.name.name}", - name = alias.name.name, - kind = "alias", - )) + result.push( + JSTypeInfo( + pkg = pkg.toString, + version = version.toString, + id = s"type:${alias.name.name}", + name = alias.name.name, + kind = "alias", + ) + ) } } } @@ -777,15 +814,13 @@ object BaboonJS { typeId: String, ): JSGenerateResult = { try { - val family = model.asInstanceOf[BaboonLoadedModelImpl].family - val parsedPkg = parsePkg(pkg) - val parsedVer = Version.parse(version) - val enquiries = new BaboonEnquiries.BaboonEnquiriesImpl + val family = model.asInstanceOf[BaboonLoadedModelImpl].family + val parsedPkg = parsePkg(pkg) + val parsedVer = Version.parse(version) + val enquiries = new BaboonEnquiries.BaboonEnquiriesImpl - val lineage = family.domains.toMap.getOrElse(parsedPkg, - throw new RuntimeException(s"Package not found: $pkg")) - val domain = lineage.versions.toMap.getOrElse(parsedVer, - throw new RuntimeException(s"Version not found: $version")) + val lineage = family.domains.toMap.getOrElse(parsedPkg, throw new RuntimeException(s"Package not found: $pkg")) + val domain = lineage.versions.toMap.getOrElse(parsedVer, throw new RuntimeException(s"Version not found: $version")) val member = domain.defs.meta.nodes.collectFirst { case (tid, m: DomainMember.User) if tid.toString == typeId => m @@ -842,10 +877,16 @@ object BaboonJS { case e: BaboonCompilationException => JSCompilationResult.failure(issuesToStructuredErrors(e.issues)) case e: Throwable => - JSCompilationResult.failure(js.Array(createJSCompilationError( - s"Compilation failed: ${e.getMessage}\n${e.getStackTrace.mkString("\n")}", - None, None, None, - ))) + JSCompilationResult.failure( + js.Array( + createJSCompilationError( + s"Compilation failed: ${e.getMessage}\n${e.getStackTrace.mkString("\n")}", + None, + None, + None, + ) + ) + ) }.toJSPromise } catch { case e: BaboonCompilationException => @@ -855,10 +896,16 @@ object BaboonJS { case e: Throwable => Future .successful( - JSCompilationResult.failure(js.Array(createJSCompilationError( - s"Compilation failed: ${e.getMessage}\n${e.getStackTrace.mkString("\n")}", - None, None, None, - ))) + JSCompilationResult.failure( + js.Array( + createJSCompilationError( + s"Compilation failed: ${e.getMessage}\n${e.getStackTrace.mkString("\n")}", + None, + None, + None, + ) + ) + ) ).toJSPromise } } @@ -925,15 +972,17 @@ object BaboonJS { m: DefaultModule[F[Throwable, _]], ): F[Throwable, Seq[OutputFileWithPath]] = { val module = target match { - case t: CompilerTargetJS.CSTarget => new BaboonJsCSModule[F](toCSTarget(t)) - case t: CompilerTargetJS.ScTarget => new BaboonJsScModule[F](toScTarget(t)) - case t: CompilerTargetJS.PyTarget => new BaboonJsPyModule[F](toPyTarget(t)) - case t: CompilerTargetJS.RsTarget => new BaboonJsRsModule[F](toRsTarget(t)) - case t: CompilerTargetJS.TsTarget => new BaboonJsTsModule[F](toTsTarget(t)) - case t: CompilerTargetJS.KtTarget => new BaboonJsKtModule[F](toKtTarget(t)) - case t: CompilerTargetJS.JvTarget => new BaboonJsJvModule[F](toJvTarget(t)) - case t: CompilerTargetJS.DtTarget => new BaboonJsDtModule[F](toDtTarget(t)) - case t: CompilerTargetJS.SwTarget => new BaboonJsSwModule[F](toSwTarget(t)) + case t: CompilerTargetJS.CSTarget => new BaboonJsCSModule[F](toCSTarget(t)) + case t: CompilerTargetJS.ScTarget => new BaboonJsScModule[F](toScTarget(t)) + case t: CompilerTargetJS.PyTarget => new BaboonJsPyModule[F](toPyTarget(t)) + case t: CompilerTargetJS.RsTarget => new BaboonJsRsModule[F](toRsTarget(t)) + case t: CompilerTargetJS.TsTarget => new BaboonJsTsModule[F](toTsTarget(t)) + case t: CompilerTargetJS.KtTarget => new BaboonJsKtModule[F](toKtTarget(t)) + case t: CompilerTargetJS.JvTarget => new BaboonJsJvModule[F](toJvTarget(t)) + case t: CompilerTargetJS.DtTarget => new BaboonJsDtModule[F](toDtTarget(t)) + case t: CompilerTargetJS.SwTarget => new BaboonJsSwModule[F](toSwTarget(t)) + case t: CompilerTargetJS.GqlTarget => new BaboonJsGqlModule[F](toGqlTarget(t)) + case t: CompilerTargetJS.OasTarget => new BaboonJsOasModule[F](toOasTarget(t)) } Injector diff --git a/baboon-compiler/.js/src/main/scala/io/septimalmind/baboon/BaboonModuleJS.scala b/baboon-compiler/.js/src/main/scala/io/septimalmind/baboon/BaboonModuleJS.scala index 3669c883..02de7063 100644 --- a/baboon-compiler/.js/src/main/scala/io/septimalmind/baboon/BaboonModuleJS.scala +++ b/baboon-compiler/.js/src/main/scala/io/septimalmind/baboon/BaboonModuleJS.scala @@ -86,6 +86,16 @@ class BaboonJsSwModule[F[+_, +_]: Error2: TagKK](compilerTarget: CompilerTarget. make[CompilerTarget.SwTarget].fromValue(compilerTarget) } +class BaboonJsGqlModule[F[+_, +_]: Error2: TagKK](compilerTarget: CompilerTarget.GqlTarget) extends ModuleDef { + include(new BaboonCommonGqlModule[F]()) + make[CompilerTarget.GqlTarget].fromValue(compilerTarget) +} + +class BaboonJsOasModule[F[+_, +_]: Error2: TagKK](compilerTarget: CompilerTarget.OasTarget) extends ModuleDef { + include(new BaboonCommonOasModule[F]()) + make[CompilerTarget.OasTarget].fromValue(compilerTarget) +} + class BaboonCodecModuleJS[F[+_, +_]: Error2: MaybeSuspend2: TagKK]( parOps: ParallelErrorAccumulatingOps2[F] ) extends ModuleDef { diff --git a/baboon-compiler/.js/src/main/scala/io/septimalmind/baboon/CompilerOptionsJS.scala b/baboon-compiler/.js/src/main/scala/io/septimalmind/baboon/CompilerOptionsJS.scala index 1f0b9376..208fe785 100644 --- a/baboon-compiler/.js/src/main/scala/io/septimalmind/baboon/CompilerOptionsJS.scala +++ b/baboon-compiler/.js/src/main/scala/io/septimalmind/baboon/CompilerOptionsJS.scala @@ -69,6 +69,20 @@ object CompilerTargetJS { generic: GenericOptions, language: SwOptions, ) extends CompilerTargetJS + + case class GqlTarget( + id: String, + output: OutputOptionsJS, + generic: GenericOptions, + language: GqlOptions, + ) extends CompilerTargetJS + + case class OasTarget( + id: String, + output: OutputOptionsJS, + generic: GenericOptions, + language: OasOptions, + ) extends CompilerTargetJS } final case class OutputOptionsJS( diff --git a/baboon-compiler/.jvm/src/main/scala/io/septimalmind/baboon/Baboon.scala b/baboon-compiler/.jvm/src/main/scala/io/septimalmind/baboon/Baboon.scala index f0b42000..0291bfc8 100644 --- a/baboon-compiler/.jvm/src/main/scala/io/septimalmind/baboon/Baboon.scala +++ b/baboon-compiler/.jvm/src/main/scala/io/septimalmind/baboon/Baboon.scala @@ -33,26 +33,40 @@ object Baboon { */ private val sharedArgNames: Set[String] = Set( // GenericTranspilerCLIOptions - "output", "fixtureOutput", "testOutput", "runtime", "disableConversions", - "omitMostRecentVersionSuffixFromPaths", "omitMostRecentVersionSuffixFromNamespaces", + "output", + "fixtureOutput", + "testOutput", + "runtime", + "disableConversions", + "omitMostRecentVersionSuffixFromPaths", + "omitMostRecentVersionSuffixFromNamespaces", "codecTestIterations", // SharedCLIOptions "extAllowCleanup", - "serviceResultNoErrors", "serviceResultType", "serviceResultPattern", - "serviceContextMode", "serviceContextType", "serviceContextParameterName", + "serviceResultNoErrors", + "serviceResultType", + "serviceResultPattern", + "serviceContextMode", + "serviceContextType", + "serviceContextParameterName", "pragma", - "generateJsonCodecs", "generateUebaCodecs", - "generateJsonCodecsByDefault", "generateUebaCodecsByDefault", + "generateJsonCodecs", + "generateUebaCodecs", + "generateJsonCodecsByDefault", + "generateUebaCodecsByDefault", "enableDeprecatedEncoders", // ScalaHktCLIOptions - "serviceResultHkt", "serviceResultHktName", "serviceResultHktSignature", + "serviceResultHkt", + "serviceResultHktName", + "serviceResultHktSignature", ) private def camelToKebab(s: String): String = { val sb = new StringBuilder - s.foreach { c => - if (c.isUpper && sb.nonEmpty) sb.append('-') - sb.append(c.toLower) + s.foreach { + c => + if (c.isUpper && sb.nonEmpty) sb.append('-') + sb.append(c.toLower) } sb.toString } @@ -64,14 +78,15 @@ object Baboon { if (languageArgs.isEmpty) return s"$label options (:$modality):\n (no language-specific options)\n" - val lines = languageArgs.map { arg => - val flag = s"--${camelToKebab(arg.name.name)}" - val value = arg.valueDescription - .map(_.description) - .filterNot(d => d == "true/false" || d == "bool?" || d == "boolean") - .fold("")(v => s" <$v>") - val desc = arg.helpMessage.fold("")(_.message) - s" $flag$value $desc" + val lines = languageArgs.map { + arg => + val flag = s"--${camelToKebab(arg.name.name)}" + val value = arg.valueDescription + .map(_.description) + .filterNot(d => d == "true/false" || d == "bool?" || d == "boolean") + .fold("")(v => s" <$v>") + val desc = arg.helpMessage.fold("")(_.message) + s" $flag$value $desc" } s"$label options (:$modality):\n${lines.mkString("\n")}\n" } @@ -89,6 +104,8 @@ object Baboon { formatLanguageHelp[JvCLIOptions]("Java", "java"), formatLanguageHelp[DtCLIOptions]("Dart", "dart"), formatLanguageHelp[SwCLIOptions]("Swift", "swift"), + formatLanguageHelp[GqlCLIOptions]("GraphQL", "graphql"), + formatLanguageHelp[OasCLIOptions]("OpenAPI", "openapi"), ).mkString("\n") s"""Usage: baboon [options] [modality-options] [ [modality-options] ...] @@ -112,6 +129,8 @@ object Baboon { | :java Generate Java 21 code | :dart Generate Dart 3+ code | :swift Generate Swift 5.9+ code + | :graphql Generate GraphQL SDL schemas + | :openapi Generate OpenAPI 3.1 component schemas | :lsp Start LSP server | :explore Start interactive explorer | :scheme Emit a cleaned-up single .baboon file for a domain version @@ -388,13 +407,15 @@ object Baboon { pragmas = parsePragmas(opts.pragma), asyncServices = opts.tsAsyncServices.getOrElse(false), mapsAsRecords = opts.tsMapsAsRecords.getOrElse(false), - timestampsUtcMode = if (opts.tsTimestampsAsStrings.getOrElse(false)) "string" - else if (opts.tsTimestampsAsDates.getOrElse(false)) "date" - else "wrapper", - timestampsOffsetMode = if (opts.tsTimestampsAsStrings.getOrElse(false)) "string" - else if (opts.tsTimestampsAsDates.getOrElse(false)) "date" - else "wrapper", - enumLowercaseValues = opts.tsEnumLowercaseValues.getOrElse(false), + timestampsUtcMode = + if (opts.tsTimestampsAsStrings.getOrElse(false)) "string" + else if (opts.tsTimestampsAsDates.getOrElse(false)) "date" + else "wrapper", + timestampsOffsetMode = + if (opts.tsTimestampsAsStrings.getOrElse(false)) "string" + else if (opts.tsTimestampsAsDates.getOrElse(false)) "date" + else "wrapper", + enumLowercaseValues = opts.tsEnumLowercaseValues.getOrElse(false), ), ) } @@ -489,6 +510,34 @@ object Baboon { ), ) } + case "graphql" => + CaseApp.parse[GqlCLIOptions](roleArgs).leftMap(e => s"Can't parse graphql CLI: $e").map { + case (opts, _) => + val shopts = mkGenericOpts(opts) + + CompilerTarget.GqlTarget( + id = "GraphQL", + output = shopts.outOpts, + generic = shopts.genericOpts, + language = GqlOptions( + pragmas = parsePragmas(opts.pragma) + ), + ) + } + case "openapi" => + CaseApp.parse[OasCLIOptions](roleArgs).leftMap(e => s"Can't parse openapi CLI: $e").map { + case (opts, _) => + val shopts = mkGenericOpts(opts) + + CompilerTarget.OasTarget( + id = "OpenAPI", + output = shopts.outOpts, + generic = shopts.genericOpts, + language = OasOptions( + pragmas = parsePragmas(opts.pragma) + ), + ) + } case r => Left(s"Unknown role id: $r") } } @@ -498,9 +547,10 @@ object Baboon { val emitOnly = generalOptions._1.emitOnly.map { raw => - raw.split(',').map(_.trim).filter(_.nonEmpty).map { - s => Pkg(NEList.unsafeFrom(s.split('.').toList)) - }.toSet + raw + .split(',').map(_.trim).filter(_.nonEmpty).map { + s => Pkg(NEList.unsafeFrom(s.split('.').toList)) + }.toSet } val options = CompilerOptions( @@ -547,7 +597,7 @@ object Baboon { val safeToRemove = NEList.from(opts.extAllowCleanup) match { case Some(value) => value.toSet - case None => Set("meta", "cs", "json", "scala", "py", "pyc", "rs", "ts", "kt", "java", "dart", "swift", "toml") + case None => Set("meta", "cs", "json", "scala", "py", "pyc", "rs", "ts", "kt", "java", "dart", "swift", "toml", "graphql") } val outOpts = OutputOptions( @@ -628,6 +678,10 @@ object Baboon { new BaboonJvmDtModule[F](t) case t: CompilerTarget.SwTarget => new BaboonJvmSwModule[F](t) + case t: CompilerTarget.GqlTarget => + new BaboonJvmGqlModule[F](t) + case t: CompilerTarget.OasTarget => + new BaboonJvmOasModule[F](t) } Injector diff --git a/baboon-compiler/.jvm/src/main/scala/io/septimalmind/baboon/BaboonModuleJvm.scala b/baboon-compiler/.jvm/src/main/scala/io/septimalmind/baboon/BaboonModuleJvm.scala index 569b925d..a00d6cd4 100644 --- a/baboon-compiler/.jvm/src/main/scala/io/septimalmind/baboon/BaboonModuleJvm.scala +++ b/baboon-compiler/.jvm/src/main/scala/io/septimalmind/baboon/BaboonModuleJvm.scala @@ -1,7 +1,7 @@ package io.septimalmind.baboon import distage.{DIKey, ModuleDef} -import io.septimalmind.baboon.CompilerTarget.{CSTarget, DtTarget, JvTarget, KtTarget, PyTarget, RsTarget, ScTarget, SwTarget, TsTarget} +import io.septimalmind.baboon.CompilerTarget.{CSTarget, DtTarget, GqlTarget, JvTarget, KtTarget, OasTarget, PyTarget, RsTarget, ScTarget, SwTarget, TsTarget} import io.septimalmind.baboon.explore.{ExploreContext, ExploreInputs} import io.septimalmind.baboon.parser.{BaboonInclusionResolver, BaboonInclusionResolverImpl} import io.septimalmind.baboon.typer.model.BaboonFamily @@ -70,6 +70,18 @@ class BaboonJvmSwModule[F[+_, +_]: Error2: TagKK](target: SwTarget) extends Modu make[SwTarget].fromValue(target) } +class BaboonJvmGqlModule[F[+_, +_]: Error2: TagKK](target: GqlTarget) extends ModuleDef { + include(new SharedTranspilerJvmModule[F]()) + include(new BaboonCommonGqlModule[F]()) + make[GqlTarget].fromValue(target) +} + +class BaboonJvmOasModule[F[+_, +_]: Error2: TagKK](target: OasTarget) extends ModuleDef { + include(new SharedTranspilerJvmModule[F]()) + include(new BaboonCommonOasModule[F]()) + make[OasTarget].fromValue(target) +} + class BaboonModuleJvm[F[+_, +_]: Error2: MaybeSuspend2: TagKK]( options: CompilerOptions, parallelAccumulatingOps2: ParallelErrorAccumulatingOps2[F], diff --git a/baboon-compiler/.jvm/src/main/scala/io/septimalmind/baboon/CLIOptions.scala b/baboon-compiler/.jvm/src/main/scala/io/septimalmind/baboon/CLIOptions.scala index 50687da3..383fb08f 100644 --- a/baboon-compiler/.jvm/src/main/scala/io/septimalmind/baboon/CLIOptions.scala +++ b/baboon-compiler/.jvm/src/main/scala/io/septimalmind/baboon/CLIOptions.scala @@ -361,6 +361,48 @@ case class DtCLIOptions( pragma: List[String], ) extends SharedCLIOptions +case class GqlCLIOptions( + @Recurse + generic: GenericTranspilerCLIOptions, + @HelpMessage("Allow to erase target directory even if files with these extensions exist there. Default: graphql,json,meta") + extAllowCleanup: List[String], + @HelpMessage("Set a pragma value (key=value, repeatable)") + pragma: List[String], + @HelpMessage("Unused, present for CLI compatibility") + serviceResultNoErrors: Option[Boolean], + @HelpMessage("Unused, present for CLI compatibility") + serviceResultType: Option[String], + @HelpMessage("Unused, present for CLI compatibility") + serviceResultPattern: Option[String], + @HelpMessage("Unused, present for CLI compatibility") + serviceContextMode: Option[String], + @HelpMessage("Unused, present for CLI compatibility") + serviceContextType: Option[String], + @HelpMessage("Unused, present for CLI compatibility") + serviceContextParameterName: Option[String], +) extends SharedCLIOptions + +case class OasCLIOptions( + @Recurse + generic: GenericTranspilerCLIOptions, + @HelpMessage("Allow to erase target directory even if files with these extensions exist there. Default: json,meta") + extAllowCleanup: List[String], + @HelpMessage("Set a pragma value (key=value, repeatable)") + pragma: List[String], + @HelpMessage("Unused, present for CLI compatibility") + serviceResultNoErrors: Option[Boolean], + @HelpMessage("Unused, present for CLI compatibility") + serviceResultType: Option[String], + @HelpMessage("Unused, present for CLI compatibility") + serviceResultPattern: Option[String], + @HelpMessage("Unused, present for CLI compatibility") + serviceContextMode: Option[String], + @HelpMessage("Unused, present for CLI compatibility") + serviceContextType: Option[String], + @HelpMessage("Unused, present for CLI compatibility") + serviceContextParameterName: Option[String], +) extends SharedCLIOptions + case class SchemeCLIOptions( @HelpMessage("Domain name (e.g., 'my.domain.name')") domain: String, diff --git a/baboon-compiler/.jvm/src/main/scala/io/septimalmind/baboon/explore/commands/ShowCommand.scala b/baboon-compiler/.jvm/src/main/scala/io/septimalmind/baboon/explore/commands/ShowCommand.scala index 809f2f76..5a610c1d 100644 --- a/baboon-compiler/.jvm/src/main/scala/io/septimalmind/baboon/explore/commands/ShowCommand.scala +++ b/baboon-compiler/.jvm/src/main/scala/io/septimalmind/baboon/explore/commands/ShowCommand.scala @@ -33,8 +33,8 @@ object ShowCommand extends Command { } def complete(args: Seq[String], ctx: ExploreContext[EitherF]): Seq[String] = { - val partial = args.lastOption.getOrElse("") - val typeNames = ctx.allTypeIds.map(_.name.name) + val partial = args.lastOption.getOrElse("") + val typeNames = ctx.allTypeIds.map(_.name.name) val aliasNames = ctx.allAliases.map(_.name.name) (typeNames ++ aliasNames) .filter(_.toLowerCase.contains(partial.toLowerCase)) diff --git a/baboon-compiler/.jvm/src/test/scala/io/septimalmind/baboon/lsp/features/LspFeaturesTest.scala b/baboon-compiler/.jvm/src/test/scala/io/septimalmind/baboon/lsp/features/LspFeaturesTest.scala index fbd74962..02c3ee3b 100644 --- a/baboon-compiler/.jvm/src/test/scala/io/septimalmind/baboon/lsp/features/LspFeaturesTest.scala +++ b/baboon-compiler/.jvm/src/test/scala/io/septimalmind/baboon/lsp/features/LspFeaturesTest.scala @@ -24,20 +24,24 @@ abstract class LspFeaturesTestBase[F[+_, +_]: Error2: TagKK: BaboonTestModule] e private val logger = BLogger.Noop /** Simulate LSP state with a real compiled family and a file opened from disk. */ - private def withLspState(loader: BaboonLoader[F], fileRelPath: String)( - fn: (DocumentState, WorkspaceState, String) => Unit + private def withLspState( + loader: BaboonLoader[F], + fileRelPath: String, + )(fn: (DocumentState, WorkspaceState, String) => Unit ): F[NEList[BaboonIssue], Unit] = { import izumi.fundamentals.platform.files.IzFiles val basePath = java.nio.file.Paths.get("./baboon-compiler/src/test/resources/baboon").toAbsolutePath.normalize() // Collect all .baboon files from the test resources - val allFiles = IzFiles.walk(basePath.toFile).toList + val allFiles = IzFiles + .walk(basePath.toFile).toList .filter(p => p.toFile.isFile && p.toFile.getName.endsWith(".baboon")) - val inputs = allFiles.map { f => - val path = f.toAbsolutePath.normalize() - val content = java.nio.file.Files.readString(path) - BaboonParser.Input(FSPath.parse(izumi.fundamentals.collections.nonempty.NEString.unsafeFrom(path.toString)), content) + val inputs = allFiles.map { + f => + val path = f.toAbsolutePath.normalize() + val content = java.nio.file.Files.readString(path) + BaboonParser.Input(FSPath.parse(izumi.fundamentals.collections.nonempty.NEString.unsafeFrom(path.toString)), content) } for { @@ -57,8 +61,8 @@ abstract class LspFeaturesTestBase[F[+_, +_]: Error2: TagKK: BaboonTestModule] e } val inputProvider = new InputProvider { def getWorkspaceInputs: Seq[BaboonParser.Input] = inputs - def pathToUri(path: String): String = pathOps.pathToUri(path) - def uriToPath(uri: String): String = pathOps.uriToPath(uri) + def pathToUri(path: String): String = pathOps.pathToUri(path) + def uriToPath(uri: String): String = pathOps.uriToPath(uri) } val wsState = new WorkspaceState(docState, compiler, inputProvider, pathOps, logger) wsState.recompile() @@ -70,56 +74,59 @@ abstract class LspFeaturesTestBase[F[+_, +_]: Error2: TagKK: BaboonTestModule] e "hover provider" should { "show hover for regular types" in { (loader: BaboonLoader[F]) => - withLspState(loader, "pkg0/pkg01.baboon") { (docState, wsState, uri) => - val hover = new HoverProvider(docState, wsState, logger) - - // Find the line containing "T1_D1" definition and hover over it - val content = docState.getContent(uri).get - val lines = content.split("\n") - val lineIdx = lines.indexWhere(_.contains("root data T1_D1")) - assert(lineIdx >= 0, "Should find T1_D1 definition line") - - val colIdx = lines(lineIdx).indexOf("T1_D1") - val result = hover.getHover(uri, Position(lineIdx, colIdx + 1)) - assert(result.isDefined, "Should return hover for T1_D1") - assert(result.get.contents.value.contains("data"), s"Hover should mention 'data': ${result.get.contents.value}") - assert(result.get.contents.value.contains("T1_D1"), s"Hover should mention 'T1_D1': ${result.get.contents.value}") + withLspState(loader, "pkg0/pkg01.baboon") { + (docState, wsState, uri) => + val hover = new HoverProvider(docState, wsState, logger) + + // Find the line containing "T1_D1" definition and hover over it + val content = docState.getContent(uri).get + val lines = content.split("\n") + val lineIdx = lines.indexWhere(_.contains("root data T1_D1")) + assert(lineIdx >= 0, "Should find T1_D1 definition line") + + val colIdx = lines(lineIdx).indexOf("T1_D1") + val result = hover.getHover(uri, Position(lineIdx, colIdx + 1)) + assert(result.isDefined, "Should return hover for T1_D1") + assert(result.get.contents.value.contains("data"), s"Hover should mention 'data': ${result.get.contents.value}") + assert(result.get.contents.value.contains("T1_D1"), s"Hover should mention 'T1_D1': ${result.get.contents.value}") } } "show hover for type aliases" in { (loader: BaboonLoader[F]) => - withLspState(loader, "pkg0/pkg01.baboon") { (docState, wsState, uri) => - val hover = new HoverProvider(docState, wsState, logger) - - val content = docState.getContent(uri).get - val lines = content.split("\n") - val lineIdx = lines.indexWhere(_.contains("type BinaryData")) - assert(lineIdx >= 0, "Should find BinaryData alias line") - - val colIdx = lines(lineIdx).indexOf("BinaryData") - val result = hover.getHover(uri, Position(lineIdx, colIdx + 1)) - assert(result.isDefined, "Should return hover for BinaryData alias") - assert(result.get.contents.value.contains("type"), s"Hover should contain 'type': ${result.get.contents.value}") - assert(result.get.contents.value.contains("BinaryData"), s"Hover should contain 'BinaryData': ${result.get.contents.value}") - assert(result.get.contents.value.contains("bytes"), s"Hover should contain target 'bytes': ${result.get.contents.value}") + withLspState(loader, "pkg0/pkg01.baboon") { + (docState, wsState, uri) => + val hover = new HoverProvider(docState, wsState, logger) + + val content = docState.getContent(uri).get + val lines = content.split("\n") + val lineIdx = lines.indexWhere(_.contains("type BinaryData")) + assert(lineIdx >= 0, "Should find BinaryData alias line") + + val colIdx = lines(lineIdx).indexOf("BinaryData") + val result = hover.getHover(uri, Position(lineIdx, colIdx + 1)) + assert(result.isDefined, "Should return hover for BinaryData alias") + assert(result.get.contents.value.contains("type"), s"Hover should contain 'type': ${result.get.contents.value}") + assert(result.get.contents.value.contains("BinaryData"), s"Hover should contain 'BinaryData': ${result.get.contents.value}") + assert(result.get.contents.value.contains("bytes"), s"Hover should contain target 'bytes': ${result.get.contents.value}") } } "show hover for collection aliases" in { (loader: BaboonLoader[F]) => - withLspState(loader, "pkg0/pkg01.baboon") { (docState, wsState, uri) => - val hover = new HoverProvider(docState, wsState, logger) - - val content = docState.getContent(uri).get - val lines = content.split("\n") - val lineIdx = lines.indexWhere(_.contains("type StringList")) - assert(lineIdx >= 0, "Should find StringList alias line") - - val colIdx = lines(lineIdx).indexOf("StringList") - val result = hover.getHover(uri, Position(lineIdx, colIdx + 1)) - assert(result.isDefined, "Should return hover for StringList alias") - assert(result.get.contents.value.contains("lst[str]"), s"Hover should contain 'lst[str]': ${result.get.contents.value}") + withLspState(loader, "pkg0/pkg01.baboon") { + (docState, wsState, uri) => + val hover = new HoverProvider(docState, wsState, logger) + + val content = docState.getContent(uri).get + val lines = content.split("\n") + val lineIdx = lines.indexWhere(_.contains("type StringList")) + assert(lineIdx >= 0, "Should find StringList alias line") + + val colIdx = lines(lineIdx).indexOf("StringList") + val result = hover.getHover(uri, Position(lineIdx, colIdx + 1)) + assert(result.isDefined, "Should return hover for StringList alias") + assert(result.get.contents.value.contains("lst[str]"), s"Hover should contain 'lst[str]': ${result.get.contents.value}") } } } @@ -127,61 +134,77 @@ abstract class LspFeaturesTestBase[F[+_, +_]: Error2: TagKK: BaboonTestModule] e "completion provider" should { "include all parser keywords" in { (loader: BaboonLoader[F]) => - withLspState(loader, "pkg0/pkg01.baboon") { (docState, wsState, uri) => - val completion = new CompletionProvider(docState, wsState, logger) - - // Find an empty line to get keyword completions - val content = docState.getContent(uri).get - val lines = content.split("\n") - val emptyLineIdx = lines.indexWhere(_.trim.isEmpty) - assert(emptyLineIdx >= 0, "Should find an empty line") - - val items = completion.getCompletions(uri, Position(emptyLineIdx, 0)) - val labels = items.map(_.label).toSet - - val expectedKeywords = Set( - "model", "version", "import", "include", "root", - "data", "struct", "adt", "enum", "foreign", - "contract", "service", "ns", "pragma", "derived", - "was", "type", - ) - expectedKeywords.foreach { kw => - assert(labels.contains(kw), s"Completion should include keyword '$kw', got: ${labels.filter(_.length < 15)}") - } + withLspState(loader, "pkg0/pkg01.baboon") { + (docState, wsState, uri) => + val completion = new CompletionProvider(docState, wsState, logger) + + // Find an empty line to get keyword completions + val content = docState.getContent(uri).get + val lines = content.split("\n") + val emptyLineIdx = lines.indexWhere(_.trim.isEmpty) + assert(emptyLineIdx >= 0, "Should find an empty line") + + val items = completion.getCompletions(uri, Position(emptyLineIdx, 0)) + val labels = items.map(_.label).toSet + + val expectedKeywords = Set( + "model", + "version", + "import", + "include", + "root", + "data", + "struct", + "adt", + "enum", + "foreign", + "contract", + "service", + "ns", + "pragma", + "derived", + "was", + "type", + ) + expectedKeywords.foreach { + kw => + assert(labels.contains(kw), s"Completion should include keyword '$kw', got: ${labels.filter(_.length < 15)}") + } } } "include builtins, user types and aliases in type position" in { (loader: BaboonLoader[F]) => - withLspState(loader, "pkg0/pkg01.baboon") { (docState, wsState, uri) => - val completion = new CompletionProvider(docState, wsState, logger) - - // Find a field declaration line to get type-position completions - val content = docState.getContent(uri).get - val lines = content.split("\n") - // Find a line like " f1: i08" and position cursor after ":" - val fieldLineIdx = lines.indexWhere(l => l.contains(": i08") || l.contains(": str") || l.contains(": i32")) - assert(fieldLineIdx >= 0, "Should find a field line") - val colonIdx = lines(fieldLineIdx).indexOf(':') - - val items = completion.getCompletions(uri, Position(fieldLineIdx, colonIdx + 2)) - val labels = items.map(_.label).toSet - - // Builtin types - assert(labels.contains("i32"), s"Should include builtin 'i32'") - assert(labels.contains("str"), s"Should include builtin 'str'") - assert(labels.contains("opt"), s"Should include builtin 'opt'") - - // User types - assert(labels.exists(_.contains("T1_D1")), "Should include user type T1_D1") - - // Aliases - assert(labels.contains("BinaryData"), "Should include BinaryData alias") - assert(labels.contains("StringList"), "Should include StringList alias") - - // Alias detail should show target - val binaryItem = items.find(_.label == "BinaryData").get - assert(binaryItem.detail.exists(_.contains("bytes")), s"BinaryData detail should mention target: ${binaryItem.detail}") + withLspState(loader, "pkg0/pkg01.baboon") { + (docState, wsState, uri) => + val completion = new CompletionProvider(docState, wsState, logger) + + // Find a field declaration line to get type-position completions + val content = docState.getContent(uri).get + val lines = content.split("\n") + // Find a line like " f1: i08" and position cursor after ":" + val fieldLineIdx = lines.indexWhere(l => l.contains(": i08") || l.contains(": str") || l.contains(": i32")) + assert(fieldLineIdx >= 0, "Should find a field line") + val colonIdx = lines(fieldLineIdx).indexOf(':') + + val items = completion.getCompletions(uri, Position(fieldLineIdx, colonIdx + 2)) + val labels = items.map(_.label).toSet + + // Builtin types + assert(labels.contains("i32"), s"Should include builtin 'i32'") + assert(labels.contains("str"), s"Should include builtin 'str'") + assert(labels.contains("opt"), s"Should include builtin 'opt'") + + // User types + assert(labels.exists(_.contains("T1_D1")), "Should include user type T1_D1") + + // Aliases + assert(labels.contains("BinaryData"), "Should include BinaryData alias") + assert(labels.contains("StringList"), "Should include StringList alias") + + // Alias detail should show target + val binaryItem = items.find(_.label == "BinaryData").get + assert(binaryItem.detail.exists(_.contains("bytes")), s"BinaryData detail should mention target: ${binaryItem.detail}") } } } @@ -189,38 +212,40 @@ abstract class LspFeaturesTestBase[F[+_, +_]: Error2: TagKK: BaboonTestModule] e "document symbol provider" should { "list types defined in a file" in { (loader: BaboonLoader[F]) => - withLspState(loader, "pkg0/pkg01.baboon") { (_, wsState, uri) => - val symbolProvider = new DocumentSymbolProvider(wsState, positionConverter, pathOps, logger) - val symbols = symbolProvider.getSymbols(uri) - - assert(symbols.nonEmpty, "Should find symbols in pkg01.baboon") - val names = symbols.map(_.name).toSet - - // Regular types - assert(names.contains("T1_D1"), s"Should include T1_D1, got: $names") - assert(names.contains("T4_A1"), s"Should include T4_A1, got: $names") - assert(names.contains("T1_E1"), s"Should include T1_E1, got: $names") - - // Aliases - assert(names.contains("BinaryData"), s"Should include BinaryData alias, got: $names") - assert(names.contains("StringList"), s"Should include StringList alias, got: $names") + withLspState(loader, "pkg0/pkg01.baboon") { + (_, wsState, uri) => + val symbolProvider = new DocumentSymbolProvider(wsState, positionConverter, pathOps, logger) + val symbols = symbolProvider.getSymbols(uri) + + assert(symbols.nonEmpty, "Should find symbols in pkg01.baboon") + val names = symbols.map(_.name).toSet + + // Regular types + assert(names.contains("T1_D1"), s"Should include T1_D1, got: $names") + assert(names.contains("T4_A1"), s"Should include T4_A1, got: $names") + assert(names.contains("T1_E1"), s"Should include T1_E1, got: $names") + + // Aliases + assert(names.contains("BinaryData"), s"Should include BinaryData alias, got: $names") + assert(names.contains("StringList"), s"Should include StringList alias, got: $names") } } "use correct symbol kinds" in { (loader: BaboonLoader[F]) => - withLspState(loader, "pkg0/pkg01.baboon") { (_, wsState, uri) => - val symbolProvider = new DocumentSymbolProvider(wsState, positionConverter, pathOps, logger) - val symbols = symbolProvider.getSymbols(uri) - val symbolMap = symbols.map(s => s.name -> s.kind).toMap - - // Verify symbol kinds match type categories - symbolMap.get("T1_E1").foreach(kind => assert(kind == SymbolKind.Enum, s"T1_E1 should be Enum, got $kind")) - symbolMap.get("T4_A1").foreach(kind => assert(kind == SymbolKind.Interface, s"T4_A1 (ADT) should be Interface, got $kind")) - symbolMap.get("ObscureInt").foreach(kind => assert(kind == SymbolKind.TypeParameter, s"ObscureInt (foreign) should be TypeParameter, got $kind")) - - // Aliases should be TypeParameter - symbolMap.get("BinaryData").foreach(kind => assert(kind == SymbolKind.TypeParameter, s"BinaryData (alias) should be TypeParameter, got $kind")) + withLspState(loader, "pkg0/pkg01.baboon") { + (_, wsState, uri) => + val symbolProvider = new DocumentSymbolProvider(wsState, positionConverter, pathOps, logger) + val symbols = symbolProvider.getSymbols(uri) + val symbolMap = symbols.map(s => s.name -> s.kind).toMap + + // Verify symbol kinds match type categories + symbolMap.get("T1_E1").foreach(kind => assert(kind == SymbolKind.Enum, s"T1_E1 should be Enum, got $kind")) + symbolMap.get("T4_A1").foreach(kind => assert(kind == SymbolKind.Interface, s"T4_A1 (ADT) should be Interface, got $kind")) + symbolMap.get("ObscureInt").foreach(kind => assert(kind == SymbolKind.TypeParameter, s"ObscureInt (foreign) should be TypeParameter, got $kind")) + + // Aliases should be TypeParameter + symbolMap.get("BinaryData").foreach(kind => assert(kind == SymbolKind.TypeParameter, s"BinaryData (alias) should be TypeParameter, got $kind")) } } } @@ -228,40 +253,42 @@ abstract class LspFeaturesTestBase[F[+_, +_]: Error2: TagKK: BaboonTestModule] e "definition provider" should { "find definitions for regular types" in { (loader: BaboonLoader[F]) => - withLspState(loader, "pkg0/pkg01.baboon") { (docState, wsState, uri) => - val defProvider = new DefinitionProvider(docState, wsState, positionConverter) - - val content = docState.getContent(uri).get - val lines = content.split("\n") - - // Find a line that uses T1_D1 as a reference (not definition) - val refLineIdx = lines.indexWhere(l => l.contains("f0: T1_D2") || l.contains("f0: T2_D2")) - if (refLineIdx >= 0) { - val line = lines(refLineIdx) - val colIdx = line.indexOf("T1_D2").max(line.indexOf("T2_D2")) - if (colIdx >= 0) { - val locations = defProvider.findDefinition(uri, Position(refLineIdx, colIdx + 1)) - assert(locations.nonEmpty, "Should find definition for referenced type") + withLspState(loader, "pkg0/pkg01.baboon") { + (docState, wsState, uri) => + val defProvider = new DefinitionProvider(docState, wsState, positionConverter) + + val content = docState.getContent(uri).get + val lines = content.split("\n") + + // Find a line that uses T1_D1 as a reference (not definition) + val refLineIdx = lines.indexWhere(l => l.contains("f0: T1_D2") || l.contains("f0: T2_D2")) + if (refLineIdx >= 0) { + val line = lines(refLineIdx) + val colIdx = line.indexOf("T1_D2").max(line.indexOf("T2_D2")) + if (colIdx >= 0) { + val locations = defProvider.findDefinition(uri, Position(refLineIdx, colIdx + 1)) + assert(locations.nonEmpty, "Should find definition for referenced type") + } } - } } } "find definitions for aliases" in { (loader: BaboonLoader[F]) => - withLspState(loader, "pkg0/pkg01.baboon") { (docState, wsState, uri) => - val defProvider = new DefinitionProvider(docState, wsState, positionConverter) + withLspState(loader, "pkg0/pkg01.baboon") { + (docState, wsState, uri) => + val defProvider = new DefinitionProvider(docState, wsState, positionConverter) - val content = docState.getContent(uri).get - val lines = content.split("\n") + val content = docState.getContent(uri).get + val lines = content.split("\n") - // Find a line that uses BinaryData as a field type - val lineIdx = lines.indexWhere(_.contains("binary: BinaryData")) - assert(lineIdx >= 0, "Should find line with BinaryData usage") - val colIdx = lines(lineIdx).indexOf("BinaryData") + // Find a line that uses BinaryData as a field type + val lineIdx = lines.indexWhere(_.contains("binary: BinaryData")) + assert(lineIdx >= 0, "Should find line with BinaryData usage") + val colIdx = lines(lineIdx).indexOf("BinaryData") - val locations = defProvider.findDefinition(uri, Position(lineIdx, colIdx + 1)) - assert(locations.nonEmpty, "Should find definition for BinaryData alias") + val locations = defProvider.findDefinition(uri, Position(lineIdx, colIdx + 1)) + assert(locations.nonEmpty, "Should find definition for BinaryData alias") } } } diff --git a/baboon-compiler/.jvm/src/test/scala/io/septimalmind/baboon/tests/BaboonTest.scala b/baboon-compiler/.jvm/src/test/scala/io/septimalmind/baboon/tests/BaboonTest.scala index e12fedcc..f613e8ee 100644 --- a/baboon-compiler/.jvm/src/test/scala/io/septimalmind/baboon/tests/BaboonTest.scala +++ b/baboon-compiler/.jvm/src/test/scala/io/septimalmind/baboon/tests/BaboonTest.scala @@ -28,7 +28,7 @@ abstract class BaboonTest[F[+_, +_]: TagKK: BaboonTestModule] extends Spec2[F]() directoryInputs = Set(FSPath.parse(NEString.unsafeFrom("./baboon-compiler/src/test/resources/baboon"))), metaWriteEvolutionJsonTo = None, lockFile = Some(FSPath.parse(NEString.unsafeFrom("./target/baboon.lock"))), - emitOnly = None, + emitOnly = None, targets = Seq( CSTarget( id = "C#", diff --git a/baboon-compiler/.jvm/src/test/scala/io/septimalmind/baboon/tests/ExplorerTest.scala b/baboon-compiler/.jvm/src/test/scala/io/septimalmind/baboon/tests/ExplorerTest.scala index 9c5a40af..373f474e 100644 --- a/baboon-compiler/.jvm/src/test/scala/io/septimalmind/baboon/tests/ExplorerTest.scala +++ b/baboon-compiler/.jvm/src/test/scala/io/septimalmind/baboon/tests/ExplorerTest.scala @@ -21,8 +21,11 @@ abstract class ExplorerTestBase[F[+_, +_]: Error2: TagKK: BaboonTestModule] exte private val v1 = Version.parse("1.0.0") private val v3 = Version.parse("3.0.0") - private def withExploreCtx(loader: BaboonLoader[F], codec: BaboonRuntimeCodec[F], version: Version = v1)( - fn: ExploreContext[F] => Unit + private def withExploreCtx( + loader: BaboonLoader[F], + codec: BaboonRuntimeCodec[F], + version: Version = v1, + )(fn: ExploreContext[F] => Unit ): F[NEList[io.septimalmind.baboon.parser.model.issues.BaboonIssue], Unit] = { for { family <- loadPkg(loader) @@ -44,49 +47,52 @@ abstract class ExplorerTestBase[F[+_, +_]: Error2: TagKK: BaboonTestModule] exte "explorer context" should { "find regular types and aliases in v1" in { (loader: BaboonLoader[F], codec: BaboonRuntimeCodec[F]) => - withExploreCtx(loader, codec, v1) { ctx => - // Regular types - assert(ctx.findType("T1_D1").isDefined, "T1_D1 should be findable") - assert(ctx.findType("T4_A1").isDefined, "T4_A1 (ADT) should be findable") - assert(ctx.findType("T1_E1").isDefined, "T1_E1 (enum) should be findable") - - // Aliases - assert(ctx.findAlias("BinaryData").isDefined, "BinaryData alias should be findable") - assert(ctx.findAlias("StringList").isDefined, "StringList alias should be findable") - assert(ctx.findAlias("ChainedAlias").isDefined, "ChainedAlias alias should be findable") - assert(ctx.findAlias("NsAlias").isDefined, "NsAlias (namespace-scoped) should be findable") - - // Non-existent - assert(ctx.findType("NonExistent").isEmpty) - assert(ctx.findAlias("NonExistent").isEmpty) - - // Aliases should not appear as regular types - assert(ctx.findType("BinaryData").isEmpty, "Alias should not be findable as a regular type") + withExploreCtx(loader, codec, v1) { + ctx => + // Regular types + assert(ctx.findType("T1_D1").isDefined, "T1_D1 should be findable") + assert(ctx.findType("T4_A1").isDefined, "T4_A1 (ADT) should be findable") + assert(ctx.findType("T1_E1").isDefined, "T1_E1 (enum) should be findable") + + // Aliases + assert(ctx.findAlias("BinaryData").isDefined, "BinaryData alias should be findable") + assert(ctx.findAlias("StringList").isDefined, "StringList alias should be findable") + assert(ctx.findAlias("ChainedAlias").isDefined, "ChainedAlias alias should be findable") + assert(ctx.findAlias("NsAlias").isDefined, "NsAlias (namespace-scoped) should be findable") + + // Non-existent + assert(ctx.findType("NonExistent").isEmpty) + assert(ctx.findAlias("NonExistent").isEmpty) + + // Aliases should not appear as regular types + assert(ctx.findType("BinaryData").isEmpty, "Alias should not be findable as a regular type") } } "list all v1 aliases" in { (loader: BaboonLoader[F], codec: BaboonRuntimeCodec[F]) => - withExploreCtx(loader, codec, v1) { ctx => - val names = ctx.allAliases.map(_.name.name).toSet - assert(names.contains("BinaryData")) - assert(names.contains("StringList")) - assert(names.contains("OptionalInt")) - assert(names.contains("IntMap")) - assert(names.contains("EnumAlias")) - assert(names.contains("AdtAlias")) - assert(names.contains("ChainedAlias")) - assert(names.contains("DoubleChain")) - assert(names.contains("NsAlias")) + withExploreCtx(loader, codec, v1) { + ctx => + val names = ctx.allAliases.map(_.name.name).toSet + assert(names.contains("BinaryData")) + assert(names.contains("StringList")) + assert(names.contains("OptionalInt")) + assert(names.contains("IntMap")) + assert(names.contains("EnumAlias")) + assert(names.contains("AdtAlias")) + assert(names.contains("ChainedAlias")) + assert(names.contains("DoubleChain")) + assert(names.contains("NsAlias")) } } "list v3 aliases" in { (loader: BaboonLoader[F], codec: BaboonRuntimeCodec[F]) => - withExploreCtx(loader, codec, v3) { ctx => - val names = ctx.allAliases.map(_.name.name).toSet - assert(names.contains("MyStr")) - assert(names.contains("MyList")) + withExploreCtx(loader, codec, v3) { + ctx => + val names = ctx.allAliases.map(_.name.name).toSet + assert(names.contains("MyStr")) + assert(names.contains("MyList")) } } } @@ -94,26 +100,28 @@ abstract class ExplorerTestBase[F[+_, +_]: Error2: TagKK: BaboonTestModule] exte "types command" should { "list both types and aliases" in { (loader: BaboonLoader[F], codec: BaboonRuntimeCodec[F]) => - withExploreCtx(loader, codec, v1) { ctx => - val result = TypesCommand.execute(Seq.empty, ctx.asInstanceOf[ExploreContext[Either]]) - assert(result.isRight, s"types command failed: ${result.left.getOrElse("")}") - val output = result.toOption.get - - assert(output.contains("T1_D1"), s"Output should contain T1_D1") - assert(output.contains("T4_A1"), s"Output should contain T4_A1") - assert(output.contains("BinaryData"), s"Output should contain BinaryData alias") - assert(output.contains("StringList"), s"Output should contain StringList alias") + withExploreCtx(loader, codec, v1) { + ctx => + val result = TypesCommand.execute(Seq.empty, ctx.asInstanceOf[ExploreContext[Either]]) + assert(result.isRight, s"types command failed: ${result.left.getOrElse("")}") + val output = result.toOption.get + + assert(output.contains("T1_D1"), s"Output should contain T1_D1") + assert(output.contains("T4_A1"), s"Output should contain T4_A1") + assert(output.contains("BinaryData"), s"Output should contain BinaryData alias") + assert(output.contains("StringList"), s"Output should contain StringList alias") } } "filter aliases by name" in { (loader: BaboonLoader[F], codec: BaboonRuntimeCodec[F]) => - withExploreCtx(loader, codec, v1) { ctx => - val result = TypesCommand.execute(Seq("Binary"), ctx.asInstanceOf[ExploreContext[Either]]) - assert(result.isRight) - val output = result.toOption.get - assert(output.contains("BinaryData"), "Should find BinaryData alias") - assert(!output.contains("StringList"), "Should not find StringList when filtering for Binary") + withExploreCtx(loader, codec, v1) { + ctx => + val result = TypesCommand.execute(Seq("Binary"), ctx.asInstanceOf[ExploreContext[Either]]) + assert(result.isRight) + val output = result.toOption.get + assert(output.contains("BinaryData"), "Should find BinaryData alias") + assert(!output.contains("StringList"), "Should not find StringList when filtering for Binary") } } } @@ -121,32 +129,35 @@ abstract class ExplorerTestBase[F[+_, +_]: Error2: TagKK: BaboonTestModule] exte "show command" should { "render regular types" in { (loader: BaboonLoader[F], codec: BaboonRuntimeCodec[F]) => - withExploreCtx(loader, codec, v1) { ctx => - val result = ShowCommand.execute(Seq("T7_Empty"), ctx.asInstanceOf[ExploreContext[Either]]) - assert(result.isRight, s"show T7_Empty failed: ${result.left.getOrElse("")}") - val output = result.toOption.get - assert(output.contains("data"), "Should render with 'data' keyword") - assert(output.contains("T7_Empty"), "Should contain type name") + withExploreCtx(loader, codec, v1) { + ctx => + val result = ShowCommand.execute(Seq("T7_Empty"), ctx.asInstanceOf[ExploreContext[Either]]) + assert(result.isRight, s"show T7_Empty failed: ${result.left.getOrElse("")}") + val output = result.toOption.get + assert(output.contains("data"), "Should render with 'data' keyword") + assert(output.contains("T7_Empty"), "Should contain type name") } } "render aliases" in { (loader: BaboonLoader[F], codec: BaboonRuntimeCodec[F]) => - withExploreCtx(loader, codec, v1) { ctx => - val result = ShowCommand.execute(Seq("BinaryData"), ctx.asInstanceOf[ExploreContext[Either]]) - assert(result.isRight, s"show BinaryData failed: ${result.left.getOrElse("")}") - val output = result.toOption.get - assert(output.contains("type"), "Should render with 'type' keyword") - assert(output.contains("BinaryData"), "Should contain alias name") - assert(output.contains("bytes"), "Should contain target type") + withExploreCtx(loader, codec, v1) { + ctx => + val result = ShowCommand.execute(Seq("BinaryData"), ctx.asInstanceOf[ExploreContext[Either]]) + assert(result.isRight, s"show BinaryData failed: ${result.left.getOrElse("")}") + val output = result.toOption.get + assert(output.contains("type"), "Should render with 'type' keyword") + assert(output.contains("BinaryData"), "Should contain alias name") + assert(output.contains("bytes"), "Should contain target type") } } "fail for non-existent types" in { (loader: BaboonLoader[F], codec: BaboonRuntimeCodec[F]) => - withExploreCtx(loader, codec, v1) { ctx => - val result = ShowCommand.execute(Seq("NonExistent"), ctx.asInstanceOf[ExploreContext[Either]]) - assert(result.isLeft, "Should fail for non-existent type") + withExploreCtx(loader, codec, v1) { + ctx => + val result = ShowCommand.execute(Seq("NonExistent"), ctx.asInstanceOf[ExploreContext[Either]]) + assert(result.isLeft, "Should fail for non-existent type") } } } @@ -154,68 +165,72 @@ abstract class ExplorerTestBase[F[+_, +_]: Error2: TagKK: BaboonTestModule] exte "type renderer" should { "render all typedef variants" in { (loader: BaboonLoader[F], codec: BaboonRuntimeCodec[F]) => - withExploreCtx(loader, codec, v1) { ctx => - val dom = ctx.currentDomain.get - val renderer = new TypeRenderer(dom) - - // DTO - val dtoRendered = renderer.render(ctx.findType("T1_D1").get) - assert(dtoRendered.contains("data"), s"DTO should render with 'data': $dtoRendered") - - // ADT - val adtRendered = renderer.render(ctx.findType("T4_A1").get) - assert(adtRendered.contains("adt"), s"ADT should render with 'adt': $adtRendered") - - // Enum - val enumRendered = renderer.render(ctx.findType("T1_E1").get) - assert(enumRendered.contains("enum"), s"Enum should render with 'enum': $enumRendered") - - // Foreign - val foreignRendered = renderer.render(ctx.findType("ObscureInt").get) - assert(foreignRendered.contains("foreign"), s"Foreign should render with 'foreign': $foreignRendered") - - // Alias - val aliasRendered = renderer.renderAlias(ctx.findAlias("BinaryData").get) - assert(aliasRendered.contains("type"), s"Alias should render with 'type': $aliasRendered") - assert(aliasRendered.contains("bytes"), s"Alias should show target: $aliasRendered") - - // Collection alias - val listAliasRendered = renderer.renderAlias(ctx.findAlias("StringList").get) - assert(listAliasRendered.contains("lst[str]"), s"Collection alias should render target: $listAliasRendered") + withExploreCtx(loader, codec, v1) { + ctx => + val dom = ctx.currentDomain.get + val renderer = new TypeRenderer(dom) + + // DTO + val dtoRendered = renderer.render(ctx.findType("T1_D1").get) + assert(dtoRendered.contains("data"), s"DTO should render with 'data': $dtoRendered") + + // ADT + val adtRendered = renderer.render(ctx.findType("T4_A1").get) + assert(adtRendered.contains("adt"), s"ADT should render with 'adt': $adtRendered") + + // Enum + val enumRendered = renderer.render(ctx.findType("T1_E1").get) + assert(enumRendered.contains("enum"), s"Enum should render with 'enum': $enumRendered") + + // Foreign + val foreignRendered = renderer.render(ctx.findType("ObscureInt").get) + assert(foreignRendered.contains("foreign"), s"Foreign should render with 'foreign': $foreignRendered") + + // Alias + val aliasRendered = renderer.renderAlias(ctx.findAlias("BinaryData").get) + assert(aliasRendered.contains("type"), s"Alias should render with 'type': $aliasRendered") + assert(aliasRendered.contains("bytes"), s"Alias should show target: $aliasRendered") + + // Collection alias + val listAliasRendered = renderer.renderAlias(ctx.findAlias("StringList").get) + assert(listAliasRendered.contains("lst[str]"), s"Collection alias should render target: $listAliasRendered") } } "render contracts and services in v3" in { (loader: BaboonLoader[F], codec: BaboonRuntimeCodec[F]) => - withExploreCtx(loader, codec, v3) { ctx => - val dom = ctx.currentDomain.get - val renderer = new TypeRenderer(dom) + withExploreCtx(loader, codec, v3) { + ctx => + val dom = ctx.currentDomain.get + val renderer = new TypeRenderer(dom) - val contractRendered = renderer.render(ctx.findType("S0").get) - assert(contractRendered.contains("contract"), s"Contract should render with 'contract': $contractRendered") + val contractRendered = renderer.render(ctx.findType("S0").get) + assert(contractRendered.contains("contract"), s"Contract should render with 'contract': $contractRendered") - val serviceRendered = renderer.render(ctx.findType("I1").get) - assert(serviceRendered.contains("service"), s"Service should render with 'service': $serviceRendered") + val serviceRendered = renderer.render(ctx.findType("I1").get) + assert(serviceRendered.contains("service"), s"Service should render with 'service': $serviceRendered") } } "use correct keywords for all types" in { (loader: BaboonLoader[F], codec: BaboonRuntimeCodec[F]) => - withExploreCtx(loader, codec, v3) { ctx => - val dom = ctx.currentDomain.get - val renderer = new TypeRenderer(dom) - - dom.defs.meta.nodes.values.collect { case u: DomainMember.User => u }.foreach { member => - val rendered = renderer.renderTypeName(member) - member.defn match { - case _: Typedef.Dto => assert(rendered.contains("data"), s"${member.id.name.name}: expected 'data' keyword") - case _: Typedef.Adt => assert(rendered.contains("adt"), s"${member.id.name.name}: expected 'adt' keyword") - case _: Typedef.Enum => assert(rendered.contains("enum"), s"${member.id.name.name}: expected 'enum' keyword") - case _: Typedef.Foreign => assert(rendered.contains("foreign"), s"${member.id.name.name}: expected 'foreign' keyword") - case _: Typedef.Contract => assert(rendered.contains("contract"), s"${member.id.name.name}: expected 'contract' keyword") - case _: Typedef.Service => assert(rendered.contains("service"), s"${member.id.name.name}: expected 'service' keyword") + withExploreCtx(loader, codec, v3) { + ctx => + val dom = ctx.currentDomain.get + val renderer = new TypeRenderer(dom) + + dom.defs.meta.nodes.values.collect { case u: DomainMember.User => u }.foreach { + member => + val rendered = renderer.renderTypeName(member) + member.defn match { + case _: Typedef.Dto => assert(rendered.contains("data"), s"${member.id.name.name}: expected 'data' keyword") + case _: Typedef.Adt => assert(rendered.contains("adt"), s"${member.id.name.name}: expected 'adt' keyword") + case _: Typedef.Enum => assert(rendered.contains("enum"), s"${member.id.name.name}: expected 'enum' keyword") + case _: Typedef.Foreign => assert(rendered.contains("foreign"), s"${member.id.name.name}: expected 'foreign' keyword") + case _: Typedef.Contract => assert(rendered.contains("contract"), s"${member.id.name.name}: expected 'contract' keyword") + case _: Typedef.Service => assert(rendered.contains("service"), s"${member.id.name.name}: expected 'service' keyword") + } } - } } } } @@ -223,22 +238,23 @@ abstract class ExplorerTestBase[F[+_, +_]: Error2: TagKK: BaboonTestModule] exte "root type alias" should { "retain the underlying type in the domain graph" in { (loader: BaboonLoader[F], codec: BaboonRuntimeCodec[F]) => - withExploreCtx(loader, codec, v1) { ctx => - val dom = ctx.currentDomain.get - - // NotReferenced is not directly a root, but `root type RetainedAlias = NotReferenced` - // should cause it to be retained in the domain graph - assert( - ctx.findType("NotReferenced").isDefined, - s"NotReferenced should be retained via root alias. " + - s"Excluded: ${dom.excludedIds.map(_.name.name)}, " + - s"Aliases: ${dom.aliases.map(a => s"${if (a.root) "root " else ""}${a.name.name}=${a.targetRepr}").mkString(", ")}" - ) - - // Verify the alias itself is marked as root - val retainedAlias = ctx.findAlias("RetainedAlias") - assert(retainedAlias.isDefined, "RetainedAlias should exist") - assert(retainedAlias.get.root, "RetainedAlias should be marked as root") + withExploreCtx(loader, codec, v1) { + ctx => + val dom = ctx.currentDomain.get + + // NotReferenced is not directly a root, but `root type RetainedAlias = NotReferenced` + // should cause it to be retained in the domain graph + assert( + ctx.findType("NotReferenced").isDefined, + s"NotReferenced should be retained via root alias. " + + s"Excluded: ${dom.excludedIds.map(_.name.name)}, " + + s"Aliases: ${dom.aliases.map(a => s"${if (a.root) "root " else ""}${a.name.name}=${a.targetRepr}").mkString(", ")}", + ) + + // Verify the alias itself is marked as root + val retainedAlias = ctx.findAlias("RetainedAlias") + assert(retainedAlias.isDefined, "RetainedAlias should exist") + assert(retainedAlias.get.root, "RetainedAlias should be marked as root") } } } diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/BaboonModule.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/BaboonModule.scala index 15b82fe1..1dcaa300 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/BaboonModule.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/BaboonModule.scala @@ -14,6 +14,8 @@ import io.septimalmind.baboon.translator.scl.* import io.septimalmind.baboon.translator.dart.* import io.septimalmind.baboon.translator.java.* import io.septimalmind.baboon.translator.kotlin.* +import io.septimalmind.baboon.translator.graphql.* +import io.septimalmind.baboon.translator.openapi.* import io.septimalmind.baboon.translator.swift.* import io.septimalmind.baboon.translator.typescript.* import io.septimalmind.baboon.typer.* @@ -173,7 +175,7 @@ class BaboonCommonRsModule[F[+_, +_]: Error2: TagKK] extends ModuleDef { make[RsFileTools].from[RsFileTools.RsFileToolsImpl] make[RsTreeTools].from[RsTreeTools.RsTreeToolsImpl] - make[RsTypes].from { (target: CompilerTarget.RsTarget) => new RsTypes(target.language.cratePrefix) } + make[RsTypes].from((target: CompilerTarget.RsTarget) => new RsTypes(target.language.cratePrefix)) make[RsTypeTranslator] makeFactory[RsConversionTranslator.Factory[F]] @@ -227,7 +229,7 @@ class BaboonCommonKtModule[F[+_, +_]: Error2: TagKK] extends ModuleDef { make[KtFileTools].from[KtFileTools.KtFileToolsImpl] make[KtTreeTools].from[KtTreeTools.KtTreeToolsImpl] - make[KtTypes].from { (target: CompilerTarget.KtTarget) => new KtTypes(target.language.multiplatform) } + make[KtTypes].from((target: CompilerTarget.KtTarget) => new KtTypes(target.language.multiplatform)) make[KtTypeTranslator] makeFactory[KtConversionTranslator.Factory[F]] @@ -316,3 +318,21 @@ class BaboonCommonSwModule[F[+_, +_]: Error2: TagKK] extends ModuleDef { many[BaboonAbstractTranslator[F]] .ref[SwBaboonTranslator[F]] } + +class BaboonCommonGqlModule[F[+_, +_]: Error2: TagKK] extends ModuleDef { + include(new SharedTranspilerModule[F]) + + make[GqlTypeTranslator] + make[GqlBaboonTranslator[F]].aliased[BaboonAbstractTranslator[F]] + many[BaboonAbstractTranslator[F]] + .ref[GqlBaboonTranslator[F]] +} + +class BaboonCommonOasModule[F[+_, +_]: Error2: TagKK] extends ModuleDef { + include(new SharedTranspilerModule[F]) + + make[OasTypeTranslator] + make[OasBaboonTranslator[F]].aliased[BaboonAbstractTranslator[F]] + many[BaboonAbstractTranslator[F]] + .ref[OasBaboonTranslator[F]] +} diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/CompilerOptions.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/CompilerOptions.scala index 1f700091..fa3edaa3 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/CompilerOptions.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/CompilerOptions.scala @@ -74,6 +74,20 @@ object CompilerTarget { generic: GenericOptions, language: SwOptions, ) extends CompilerTarget + + case class GqlTarget( + id: String, + output: OutputOptions, + generic: GenericOptions, + language: GqlOptions, + ) extends CompilerTarget + + case class OasTarget( + id: String, + output: OutputOptions, + generic: GenericOptions, + language: OasOptions, + ) extends CompilerTarget } final case class HktConfig( @@ -289,6 +303,14 @@ final case class SwOptions( pragmas: Map[String, String], ) +final case class GqlOptions( + pragmas: Map[String, String] +) + +final case class OasOptions( + pragmas: Map[String, String] +) + final case class GenericOptions( codecTestIterations: Int ) diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/lsp/features/DiagnosticsProvider.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/lsp/features/DiagnosticsProvider.scala index 230bb7ab..8dd2b3a9 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/lsp/features/DiagnosticsProvider.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/lsp/features/DiagnosticsProvider.scala @@ -88,7 +88,7 @@ class DiagnosticsProvider(positionConverter: PositionConverter) { case ScopedRefToNamespacedGeneric(prefix, meta) => (Some(meta), s"Scoped ref to namespaced generic: ${prefix.map(_.name).mkString(".")}") case UnexpectedScopeLookup(_, meta) => (Some(meta), "Unexpected scope lookup") case NamSeqeNotFound(names, _, meta) => (Some(meta), s"Names not found: ${names.map(_.name).mkString(".")}") - case CircularAlias(name, meta) => (Some(meta), s"Circular type alias: ${name.name}") + case CircularAlias(name, meta) => (Some(meta), s"Circular type alias: ${name.name}") case DuplicatedTypes(dupes, meta) => (Some(meta), s"Duplicate types: ${dupes.map(_.name.name).mkString(", ")}") case WrongParent(id, parent, meta) => (Some(meta), s"Wrong parent for ${id.name.name}: ${parent.name.name}") case MissingContractFields(id, fields, meta) => (Some(meta), s"Missing contract fields in ${id.name.name}: ${fields.map(_.name.name).mkString(", ")}") diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/scheme/BaboonSchemeRenderer.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/scheme/BaboonSchemeRenderer.scala index e04a1c80..34c3e457 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/scheme/BaboonSchemeRenderer.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/scheme/BaboonSchemeRenderer.scala @@ -65,9 +65,10 @@ object BaboonSchemeRenderer { // Render aliases grouped by namespace val (toplevelAliases, _) = domain.aliases.partition(_.owner == Owner.Toplevel) - toplevelAliases.sortBy(_.name.name).foreach { a => - val rootPrefix = if (a.root) "root " else "" - sb.append(s"\n${rootPrefix}type ${a.name.name} = ${a.targetRepr}") + toplevelAliases.sortBy(_.name.name).foreach { + a => + val rootPrefix = if (a.root) "root " else "" + sb.append(s"\n${rootPrefix}type ${a.name.name} = ${a.targetRepr}") } renderGroup(sb, grouped, userNodes, reverseDeps, domain, indent = "") @@ -223,14 +224,16 @@ object BaboonSchemeRenderer { sb.append(s"\n${indent}ns $name {\n") val childPath = nsPath :+ name // Render aliases in this namespace - domain.aliases.filter { a => - a.owner match { - case Owner.Ns(path) => path.map(_.name).toList == childPath - case _ => false - } - }.sortBy(_.name.name).foreach { a => - val rootPrefix = if (a.root) "root " else "" - sb.append(s"${indent} ${rootPrefix}type ${a.name.name} = ${a.targetRepr}\n") + domain.aliases.filter { + a => + a.owner match { + case Owner.Ns(path) => path.map(_.name).toList == childPath + case _ => false + } + }.sortBy(_.name.name).foreach { + a => + val rootPrefix = if (a.root) "root " else "" + sb.append(s"$indent ${rootPrefix}type ${a.name.name} = ${a.targetRepr}\n") } renderGroup(sb, children, userNodes, reverseDeps, domain, indent + " ", childPath) sb.append(s"$indent}\n") diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/csharp/CSServiceWiringTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/csharp/CSServiceWiringTranslator.scala index 5742347a..6abc2590 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/csharp/CSServiceWiringTranslator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/csharp/CSServiceWiringTranslator.scala @@ -30,18 +30,22 @@ object CSServiceWiringTranslator { ServiceContextResolver.resolve(domain, "cs", target.language.serviceContext, target.language.pragmas) private def hasActiveJsonCodecs(service: Typedef.Service): Boolean = { - codecs.exists { c => - c.isInstanceOf[CSJsonCodecGenerator] && service.methods.forall { m => - c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) - } + codecs.exists { + c => + c.isInstanceOf[CSJsonCodecGenerator] && service.methods.forall { + m => + c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) + } } } private def hasActiveUebaCodecs(service: Typedef.Service): Boolean = { - codecs.exists { c => - c.isInstanceOf[CSUEBACodecGenerator] && service.methods.forall { m => - c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) - } + codecs.exists { + c => + c.isInstanceOf[CSUEBACodecGenerator] && service.methods.forall { + m => + c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) + } } } diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/dart/DtBaboonTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/dart/DtBaboonTranslator.scala index 3a1cad07..a75b3fb2 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/dart/DtBaboonTranslator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/dart/DtBaboonTranslator.scala @@ -219,14 +219,16 @@ class DtBaboonTranslator[F[+_, +_]: Error2]( } private def buildFqPrefixMap(fqTypes: Seq[DtValue.DtType]): Map[String, String] = { - fqTypes.map { t => - val key = fqFileKey(t) - val fileName = t.importAs.getOrElse(trans.toSnakeCase(t.name)) - val pkgParts = t.pkg.parts.toList - val versionIdx = pkgParts.indexWhere(p => p.startsWith("v") && p.length > 1 && p.lift(1).exists(_.isDigit)) - val prefixParts = if (versionIdx >= 0) pkgParts.drop(versionIdx) :+ fileName - else pkgParts :+ fileName - key -> prefixParts.mkString("_") + fqTypes.map { + t => + val key = fqFileKey(t) + val fileName = t.importAs.getOrElse(trans.toSnakeCase(t.name)) + val pkgParts = t.pkg.parts.toList + val versionIdx = pkgParts.indexWhere(p => p.startsWith("v") && p.length > 1 && p.lift(1).exists(_.isDigit)) + val prefixParts = + if (versionIdx >= 0) pkgParts.drop(versionIdx) :+ fileName + else pkgParts :+ fileName + key -> prefixParts.mkString("_") }.toMap } diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/dart/DtConversionTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/dart/DtConversionTranslator.scala index 5372a886..ec72b987 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/dart/DtConversionTranslator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/dart/DtConversionTranslator.scala @@ -41,7 +41,7 @@ class DtConversionTranslator[F[+_, +_]: Error2]( oldRef: TextTree[DtValue], depth: Int, maybeOldTpe: Option[TypeRef] = None, - isPromotable: Boolean = false, + isPromotable: Boolean = false, ): TextTree[DtValue] = { import io.septimalmind.baboon.translator.FQNSymbol.* diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/dart/DtServiceWiringTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/dart/DtServiceWiringTranslator.scala index e492a909..53c05f7c 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/dart/DtServiceWiringTranslator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/dart/DtServiceWiringTranslator.scala @@ -28,18 +28,22 @@ object DtServiceWiringTranslator { ServiceContextResolver.resolve(domain, "dart", target.language.serviceContext, target.language.pragmas) private def hasActiveJsonCodecs(service: Typedef.Service): Boolean = { - codecs.exists { c => - c.id == "Json" && service.methods.forall { m => - c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) - } + codecs.exists { + c => + c.id == "Json" && service.methods.forall { + m => + c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) + } } } private def hasActiveUebaCodecs(service: Typedef.Service): Boolean = { - codecs.exists { c => - c.id == "Ueba" && service.methods.forall { m => - c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) - } + codecs.exists { + c => + c.id == "Ueba" && service.methods.forall { + m => + c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) + } } } diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/graphql/GqlBaboonTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/graphql/GqlBaboonTranslator.scala new file mode 100644 index 00000000..b7813c02 --- /dev/null +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/graphql/GqlBaboonTranslator.scala @@ -0,0 +1,274 @@ +package io.septimalmind.baboon.translator.graphql + +import io.septimalmind.baboon.CompilerProduct +import io.septimalmind.baboon.CompilerTarget.GqlTarget +import io.septimalmind.baboon.parser.model.issues.BaboonIssue +import io.septimalmind.baboon.translator.{BaboonAbstractTranslator, OutputFile, Sources} +import io.septimalmind.baboon.typer.model.* +import izumi.functional.bio.{Error2, F} +import izumi.fundamentals.collections.IzCollections.* +import izumi.fundamentals.collections.nonempty.NEList +import io.septimalmind.baboon.parser.model.issues.TranslationIssue + +/** Translates Baboon domain models to GraphQL Schema Definition Language (SDL). + * + * Emits one `schema.graphql` file per domain version. Schema-only — no codecs, + * conversions, or runtime are generated. + * + * === Type mapping conventions === + * + * - '''DTO''' → `type` + * - '''Enum''' → `enum` + * - '''ADT''' → branch DTOs emitted as `type`s, then a `union` over them. + * Contracts and services inside ADTs are excluded from the union. + * - '''Foreign with `rt`''' → resolved to the underlying Baboon type + * - '''Foreign without `rt`''' → emitted as a custom `scalar` + * - '''Service / Contract''' → skipped (non-data types) + * - '''Type aliases''' → transparent, resolved by the typer before we see them + * + * === Scalar mapping === + * + * - `bit` → `Boolean`, `str` → `String`, `uid` → `ID` + * - `i08`/`i16`/`i32`/`u08`/`u16`/`u32` → `Int` + * - `f32`/`f64` → `Float` + * - `i64` → `BaboonInt64`, `u64` → `BaboonUInt64`, `f128` → `BaboonFloat128` + * - `tsu` → `BaboonDateTimeUtc`, `tso` → `BaboonDateTimeOffset` + * - `bytes` → `BaboonBytes` + * + * Custom scalars (`Baboon*`) are only emitted when actually referenced. + * + * === Collection mapping === + * + * - `opt[T]` → nullable field (no `!` suffix) + * - `lst[T]` / `set[T]` → `[T!]!` (non-null list of non-null elements) + * - `map[K, V]` → `[BaboonMapEntry_K_V!]!` with a helper type: + * {{{type BaboonMapEntry_K_V { key: K!, value: V! }}}} + * GraphQL has no native map type so this is the standard workaround. + * + * === Limitations === + * + * - No `Query` root type is emitted — the output is a type-definition library, + * not an executable schema. + * - Empty DTOs get a placeholder `_empty: Boolean` field (GraphQL forbids + * empty object types). + * - Narrowing of integer/float widths (e.g. `u08` vs `i32` both → `Int`) + * loses precision information at the schema level. + * - Field names and enum values are sanitized for GraphQL validity: + * names starting with digits get a `_` prefix, `__` prefix is escaped to `gql___`, + * and enum values `true`/`false`/`null` get a trailing `_`. + */ +class GqlBaboonTranslator[F[+_, +_]: Error2]( + target: GqlTarget, + typeTranslator: GqlTypeTranslator, +) extends BaboonAbstractTranslator[F] { + + override def translate(family: BaboonFamily): F[NEList[BaboonIssue], Sources] = { + for { + rendered <- F.fromEither { + val allFiles = family.domains.iterator.flatMap { + case (_, lineage) => + lineage.versions.iterator.map { + case (_, domain) => translateDomain(domain) + } + }.toList + + val flattened = allFiles.flatMap(identity) + + flattened.toUniqueMap(c => + BaboonIssue.of(TranslationIssue.NonUniqueOutputFiles(c)) + ) + } + } yield Sources(rendered) + } + + private def translateDomain(domain: Domain): List[(String, OutputFile)] = { + if (!target.output.products.contains(CompilerProduct.Definition)) { + return List.empty + } + + val pkg = domain.id + val ver = domain.version + val foreignResolutions = typeTranslator.foreignTypeResolution(domain) + + val members = domain.defs.meta.nodes.values.collect { + case u: DomainMember.User => u + }.toList.sortBy(_.id.toString) + + // Resolve all field type refs through foreign type mappings + def resolvedFields(fields: List[Field]): List[Field] = { + fields.map(f => f.copy(tpe = typeTranslator.resolveTypeRef(f.tpe, foreignResolutions))) + } + + // Collect all map types used in fields (after resolution) so we can emit helper types + val allMapTypes = members.flatMap { m => + m.defn match { + case dto: Typedef.Dto => + resolvedFields(dto.fields).flatMap(f => typeTranslator.collectMapTypes(f.tpe)) + case _ => Nil + } + }.toSet + + // Collect foreign types without runtime mapping — emit as custom scalars + val foreignScalars = members.flatMap { m => + m.defn match { + case f: Typedef.Foreign if f.runtimeMapping.isEmpty => Some(typeTranslator.typeName(f.id)) + case _ => None + } + }.sorted.distinct + + val sb = new StringBuilder + sb.append(s"# Generated by Baboon compiler\n") + sb.append(s"# Domain: $pkg, Version: $ver\n\n") + + // Emit custom scalars for builtin overflows + val builtinCustomScalars = Set( + "BaboonInt64", + "BaboonUInt64", + "BaboonFloat128", + "BaboonDateTimeUtc", + "BaboonDateTimeOffset", + "BaboonBytes", + ) + + val usedScalars = collectUsedCustomScalars(members, foreignResolutions) + val scalarsToEmit = (builtinCustomScalars.intersect(usedScalars).toList ++ foreignScalars).sorted + + scalarsToEmit.foreach { s => + sb.append(s"scalar $s\n") + } + if (scalarsToEmit.nonEmpty) sb.append("\n") + + // Emit map entry types (deduplicated by generated name since different TypeRefs can map to the same GraphQL type) + val mapEntries = allMapTypes.toList.map { + case (keyRef, valRef) => + (typeTranslator.mapEntryTypeName(keyRef, valRef), typeTranslator.fieldTypeStr(keyRef), typeTranslator.fieldTypeStr(valRef)) + }.distinctBy(_._1).sortBy(_._1) + + mapEntries.foreach { + case (name, keyType, valType) => + sb.append(s"type $name {\n") + sb.append(s" key: $keyType\n") + sb.append(s" value: $valType\n") + sb.append(s"}\n\n") + } + + // Emit type definitions (skip ADT-owned types — they are emitted by their parent ADT) + members.foreach { m => + m.defn match { + case _: Typedef.NonDataTypedef => // skip services, contracts + case _: Typedef.Foreign => // skip — handled as scalars or resolved via runtimeMapping + case _ if m.ownedByAdt => // skip — emitted by parent ADT + case defn => emitTypedef(sb, defn, domain, foreignResolutions) + } + } + + val versionStr = ver.toString.replace(".", "_") + val pkgStr = pkg.path.toList.map(typeTranslator.sanitize).mkString("_") + val filename = s"$pkgStr/v$versionStr/schema.graphql" + + List(filename -> OutputFile(sb.toString, CompilerProduct.Definition)) + } + + private def emitTypedef( + sb: StringBuilder, + defn: Typedef.User, + domain: Domain, + foreignResolutions: Map[TypeId.User, Option[TypeRef]], + ): Unit = { + defn match { + case dto: Typedef.Dto => + emitDto(sb, dto, foreignResolutions) + + case e: Typedef.Enum => + emitEnum(sb, e) + + case adt: Typedef.Adt => + emitAdt(sb, adt, domain, foreignResolutions) + + case _ => // skip + } + } + + private def resolveFieldType(ref: TypeRef, foreignResolutions: Map[TypeId.User, Option[TypeRef]]): String = { + typeTranslator.fieldTypeStr(typeTranslator.resolveTypeRef(ref, foreignResolutions)) + } + + private def emitDto(sb: StringBuilder, dto: Typedef.Dto, foreignResolutions: Map[TypeId.User, Option[TypeRef]]): Unit = { + val name = typeTranslator.typeName(dto.id) + if (dto.fields.isEmpty) { + sb.append(s"type $name {\n") + sb.append(s" _empty: Boolean\n") + sb.append(s"}\n\n") + } else { + sb.append(s"type $name {\n") + dto.fields.foreach { f => + sb.append(s" ${typeTranslator.sanitizeName(f.name.name)}: ${resolveFieldType(f.tpe, foreignResolutions)}\n") + } + sb.append(s"}\n\n") + } + } + + private def emitEnum(sb: StringBuilder, e: Typedef.Enum): Unit = { + val name = typeTranslator.typeName(e.id) + sb.append(s"enum $name {\n") + e.members.toList.foreach { m => + sb.append(s" ${typeTranslator.sanitizeEnumValue(m.name)}\n") + } + sb.append(s"}\n\n") + } + + private def emitAdt( + sb: StringBuilder, + adt: Typedef.Adt, + domain: Domain, + foreignResolutions: Map[TypeId.User, Option[TypeRef]], + ): Unit = { + import Typedef.Adt.AdtSyntax + val name = typeTranslator.typeName(adt.id) + + // Only emit data members (skip contracts and services) + val dataMembers = adt.dataMembers(domain) + + // Emit each branch as a type (Baboon grammar only allows DTOs as ADT branches) + val branchNames = dataMembers.map { memberId => + val branchName = typeTranslator.typeName(memberId) + domain.defs.meta.nodes.get(memberId).foreach { + case u: DomainMember.User => + u.defn match { + case dto: Typedef.Dto => emitDto(sb, dto, foreignResolutions) + case _ => // skip non-DTO members (shouldn't happen per grammar) + } + case _ => // skip + } + branchName + } + + sb.append(s"union $name = ${branchNames.mkString(" | ")}\n\n") + } + + private def collectUsedCustomScalars( + members: List[DomainMember.User], + foreignResolutions: Map[TypeId.User, Option[TypeRef]], + ): Set[String] = { + members.flatMap { m => + m.defn match { + case dto: Typedef.Dto => + dto.fields.flatMap(f => collectScalarsFromRef(typeTranslator.resolveTypeRef(f.tpe, foreignResolutions))) + case adt: Typedef.Adt => + adt.fields.flatMap(f => collectScalarsFromRef(typeTranslator.resolveTypeRef(f.tpe, foreignResolutions))) + case _ => Nil + } + }.toSet + } + + private def collectScalarsFromRef(ref: TypeRef): Set[String] = { + ref match { + case TypeRef.Scalar(id: TypeId.BuiltinScalar) => + Set(typeTranslator.scalarName(id)).filter(_.startsWith("Baboon")) + case TypeRef.Constructor(_, args) => + args.toList.flatMap(collectScalarsFromRef).toSet + case _ => + Set.empty + } + } +} diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/graphql/GqlTypeTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/graphql/GqlTypeTranslator.scala new file mode 100644 index 00000000..08b8115a --- /dev/null +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/graphql/GqlTypeTranslator.scala @@ -0,0 +1,147 @@ +package io.septimalmind.baboon.translator.graphql + +import io.septimalmind.baboon.typer.model.* + +class GqlTypeTranslator { + + def foreignTypeResolution(domain: Domain): Map[TypeId.User, Option[TypeRef]] = { + domain.defs.meta.nodes.values.collect { + case u: DomainMember.User => + u.defn match { + case f: Typedef.Foreign => Some(f.id -> f.runtimeMapping) + case _ => None + } + }.flatten.toMap + } + + def resolveTypeRef(ref: TypeRef, foreignResolutions: Map[TypeId.User, Option[TypeRef]]): TypeRef = { + ref match { + case TypeRef.Scalar(id: TypeId.User) => + foreignResolutions.get(id) match { + case Some(Some(resolved)) => resolveTypeRef(resolved, foreignResolutions) + case _ => ref + } + case TypeRef.Constructor(id, args) => + TypeRef.Constructor(id, args.map(a => resolveTypeRef(a, foreignResolutions))) + case _ => ref + } + } + + def scalarName(id: TypeId.BuiltinScalar): String = { + id match { + case TypeId.Builtins.bit => "Boolean" + case TypeId.Builtins.str => "String" + case TypeId.Builtins.i08 => "Int" + case TypeId.Builtins.i16 => "Int" + case TypeId.Builtins.i32 => "Int" + case TypeId.Builtins.i64 => "BaboonInt64" + case TypeId.Builtins.u08 => "Int" + case TypeId.Builtins.u16 => "Int" + case TypeId.Builtins.u32 => "Int" + case TypeId.Builtins.u64 => "BaboonUInt64" + case TypeId.Builtins.f32 => "Float" + case TypeId.Builtins.f64 => "Float" + case TypeId.Builtins.f128 => "BaboonFloat128" + case TypeId.Builtins.uid => "ID" + case TypeId.Builtins.tsu => "BaboonDateTimeUtc" + case TypeId.Builtins.tso => "BaboonDateTimeOffset" + case TypeId.Builtins.bytes => "BaboonBytes" + case other => s"BaboonUnknown_${other.name.name}" + } + } + + def typeRefStr(ref: TypeRef): String = { + ref match { + case TypeRef.Scalar(id: TypeId.BuiltinScalar) => + scalarName(id) + case TypeRef.Scalar(id: TypeId.User) => + typeName(id) + case TypeRef.Constructor(TypeId.Builtins.opt, args) => + typeRefStr(args.head) + case TypeRef.Constructor(TypeId.Builtins.lst, args) => + s"[${typeRefStr(args.head)}!]" + case TypeRef.Constructor(TypeId.Builtins.set, args) => + s"[${typeRefStr(args.head)}!]" + case TypeRef.Constructor(TypeId.Builtins.map, args) => + s"[${mapEntryTypeName(args.head, args.tail.head)}!]" + case other => + s"BaboonUnknown_${other.id.name.name}" + } + } + + def isOptional(ref: TypeRef): Boolean = { + ref match { + case TypeRef.Constructor(TypeId.Builtins.opt, _) => true + case _ => false + } + } + + def fieldTypeStr(ref: TypeRef): String = { + val base = typeRefStr(ref) + if (isOptional(ref)) base else s"$base!" + } + + def typeName(id: TypeId.User): String = { + val parts = id.pkg.path.toList ++ id.owner.asPseudoPkg :+ id.name.name + parts.map(sanitize).mkString("_") + } + + def sanitize(s: String): String = { + s.replace("-", "_").replace(".", "_") + } + + /** Ensure a name is a valid GraphQL identifier: `[_A-Za-z][_0-9A-Za-z]*`. + * Prepends `_` if it starts with a digit, replaces non-alphanumeric chars with `_`, + * and escapes the `__` introspection prefix. + */ + def sanitizeName(s: String): String = { + val cleaned = s.map(c => if (c.isLetterOrDigit || c == '_') c else '_') + val prefixed = if (cleaned.nonEmpty && cleaned.head.isDigit) s"_$cleaned" else cleaned + if (prefixed.startsWith("__")) s"gql_$prefixed" else prefixed + } + + private val forbiddenEnumValues = Set("true", "false", "null") + + /** Sanitize an enum member name for GraphQL: valid identifier + not a forbidden literal. */ + def sanitizeEnumValue(s: String): String = { + val cleaned = sanitizeName(s) + if (forbiddenEnumValues.contains(cleaned)) s"${cleaned}_" else cleaned + } + + /** Flat identifier for a type ref, suitable for embedding in GraphQL type names. */ + def typeRefIdent(ref: TypeRef): String = { + ref match { + case TypeRef.Scalar(id: TypeId.BuiltinScalar) => + scalarName(id) + case TypeRef.Scalar(id: TypeId.User) => + typeName(id) + case TypeRef.Constructor(TypeId.Builtins.opt, args) => + s"Opt_${typeRefIdent(args.head)}" + case TypeRef.Constructor(TypeId.Builtins.lst, args) => + s"Lst_${typeRefIdent(args.head)}" + case TypeRef.Constructor(TypeId.Builtins.set, args) => + s"Set_${typeRefIdent(args.head)}" + case TypeRef.Constructor(TypeId.Builtins.map, args) => + s"Map_${typeRefIdent(args.head)}_${typeRefIdent(args.tail.head)}" + case other => + s"Unknown_${other.id.name.name}" + } + } + + def mapEntryTypeName(keyRef: TypeRef, valRef: TypeRef): String = { + s"BaboonMapEntry_${typeRefIdent(keyRef)}_${typeRefIdent(valRef)}" + } + + def collectMapTypes(ref: TypeRef): Set[(TypeRef, TypeRef)] = { + ref match { + case TypeRef.Constructor(TypeId.Builtins.map, args) => + Set((args.head, args.tail.head)) ++ + collectMapTypes(args.head) ++ + collectMapTypes(args.tail.head) + case TypeRef.Constructor(_, args) => + args.toList.flatMap(collectMapTypes).toSet + case _ => + Set.empty + } + } +} diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/java/JvServiceWiringTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/java/JvServiceWiringTranslator.scala index 1cfe288c..93716c34 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/java/JvServiceWiringTranslator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/java/JvServiceWiringTranslator.scala @@ -28,18 +28,22 @@ object JvServiceWiringTranslator { ServiceContextResolver.resolve(domain, "java", target.language.serviceContext, target.language.pragmas) private def hasActiveJsonCodecs(service: Typedef.Service): Boolean = { - codecs.exists { c => - c.isInstanceOf[JvJsonCodecGenerator] && service.methods.forall { m => - c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) - } + codecs.exists { + c => + c.isInstanceOf[JvJsonCodecGenerator] && service.methods.forall { + m => + c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) + } } } private def hasActiveUebaCodecs(service: Typedef.Service): Boolean = { - codecs.exists { c => - c.isInstanceOf[JvUEBACodecGenerator] && service.methods.forall { m => - c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) - } + codecs.exists { + c => + c.isInstanceOf[JvUEBACodecGenerator] && service.methods.forall { + m => + c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) + } } } diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtBaboonTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtBaboonTranslator.scala index 67f8006b..b933f92a 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtBaboonTranslator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtBaboonTranslator.scala @@ -237,11 +237,12 @@ class KtBaboonTranslator[F[+_, +_]: Error2]( if (jsonEnabled) content else { var inJsonSection = false - content.linesIterator.flatMap { line => - if (line.trim == "// @baboon:json-start") { inJsonSection = true; None } - else if (line.trim == "// @baboon:json-end") { inJsonSection = false; None } - else if (inJsonSection) None - else Some(line) + content.linesIterator.flatMap { + line => + if (line.trim == "// @baboon:json-start") { inJsonSection = true; None } + else if (line.trim == "// @baboon:json-end") { inJsonSection = false; None } + else if (inJsonSection) None + else Some(line) }.mkString("\n") } } @@ -302,8 +303,8 @@ class KtBaboonTranslator[F[+_, +_]: Error2]( val conversionRegs = convs.flatMap(_.reg.iterator.toSeq).toSeq val missing = convs.flatMap(_.missing.iterator.toSeq).toSeq - val traitSuppress = if (missing.nonEmpty) "@Suppress(\"DEPRECATION\") " else "" - val classSuppress = if (conversionRegs.nonEmpty) "@Suppress(\"DEPRECATION\") " else "" + val traitSuppress = if (missing.nonEmpty) "@Suppress(\"DEPRECATION\") " else "" + val classSuppress = if (conversionRegs.nonEmpty) "@Suppress(\"DEPRECATION\") " else "" val isLatestVersion = domain.version == lineage.evolution.latest val codecSuppress = if (!isLatestVersion) "@Suppress(\"DEPRECATION\") " else "" val converter = @@ -321,8 +322,8 @@ class KtBaboonTranslator[F[+_, +_]: Error2]( |}""".stripMargin import izumi.fundamentals.collections.IzCollections.* - val regsMap = defnOut.flatMap(_.codecReg).toMultimap.view.mapValues(_.flatten).toMap - .filter { case (codecId, _) => codecId != "Json" || target.language.generateJsonCodecs } + val regsMap = + defnOut.flatMap(_.codecReg).toMultimap.view.mapValues(_.flatten).toMap.filter { case (codecId, _) => codecId != "Json" || target.language.generateJsonCodecs } val codecs = regsMap.map { case (codecId, regs) => diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtCodecTestsTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtCodecTestsTranslator.scala index ff83f27a..e0994163 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtCodecTestsTranslator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtCodecTestsTranslator.scala @@ -98,22 +98,22 @@ object KtCodecTestsTranslator { | |private fun uebaCompare(context: $baboonCodecContext, fixture: $srcRef, clue: String): ByteArray { | ${if (ktTypes.multiplatform) { - q"""val dos = $binaryOutput() - | $codecName.instance.encode(context, dos, fixture) - | - | val bytes = dos.toByteArray() - | - | val dis = $binaryInput(bytes)""".stripMargin - } else { - q"""val baos = $byteArrayOutputStream() - | val dos = $binaryOutput(baos) - | $codecName.instance.encode(context, dos, fixture) - | - | val bytes = baos.toByteArray() - | - | val bais = $byteArrayInputStream(bytes) - | val dis = $binaryInput(bais)""".stripMargin - }} + q"""val dos = $binaryOutput() + | $codecName.instance.encode(context, dos, fixture) + | + | val bytes = dos.toByteArray() + | + | val dis = $binaryInput(bytes)""".stripMargin + } else { + q"""val baos = $byteArrayOutputStream() + | val dos = $binaryOutput(baos) + | $codecName.instance.encode(context, dos, fixture) + | + | val bytes = baos.toByteArray() + | + | val bais = $byteArrayInputStream(bytes) + | val dis = $binaryInput(bais)""".stripMargin + }} | val dec = $codecName.instance.decode(context, dis) | $ktAssertEquals(fixture, dec, clue) | diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtDomainTreeTools.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtDomainTreeTools.scala index 17a36331..a559e52e 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtDomainTreeTools.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtDomainTreeTools.scala @@ -68,7 +68,7 @@ object KtDomainTreeTools { ) val baboonAdtType = MetaField( q"val baboonAdtType: $javaClass<*>", - q"${typeTranslator.asKtType(id, domain, evolution)}${classRefSuffix}", + q"${typeTranslator.asKtType(id, domain, evolution)}$classRefSuffix", q"$adtRef.baboonAdtType", ) List(adtTypeIdentifier, baboonAdtType) diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtJsonCodecGenerator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtJsonCodecGenerator.scala index 485825dd..797f4e52 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtJsonCodecGenerator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtJsonCodecGenerator.scala @@ -272,23 +272,25 @@ class KtJsonCodecGenerator( tpe match { case TypeRef.Scalar(id) => id match { - case TypeId.Builtins.bit => q"$ref.jsonPrimitive.boolean" - case TypeId.Builtins.i08 => q"$ref.jsonPrimitive.int.toByte()" - case TypeId.Builtins.i16 => q"$ref.jsonPrimitive.int.toShort()" - case TypeId.Builtins.i32 => q"$ref.jsonPrimitive.int" - case TypeId.Builtins.i64 => q"$ref.jsonPrimitive.long" - case TypeId.Builtins.u08 => q"$ref.jsonPrimitive.int.toUByte()" - case TypeId.Builtins.u16 => q"$ref.jsonPrimitive.int.toUShort()" - case TypeId.Builtins.u32 => q"$ref.jsonPrimitive.long.toUInt()" - case TypeId.Builtins.u64 => q"$ref.jsonPrimitive.long.toULong()" - case TypeId.Builtins.f32 => q"$ref.jsonPrimitive.float" - case TypeId.Builtins.f64 => q"$ref.jsonPrimitive.double" - case TypeId.Builtins.f128 => if (ktTypes.multiplatform) q"${ktTypes.ktBigDecimal}.fromString($ref.jsonPrimitive.content)" else q"java.math.BigDecimal($ref.jsonPrimitive.content)" + case TypeId.Builtins.bit => q"$ref.jsonPrimitive.boolean" + case TypeId.Builtins.i08 => q"$ref.jsonPrimitive.int.toByte()" + case TypeId.Builtins.i16 => q"$ref.jsonPrimitive.int.toShort()" + case TypeId.Builtins.i32 => q"$ref.jsonPrimitive.int" + case TypeId.Builtins.i64 => q"$ref.jsonPrimitive.long" + case TypeId.Builtins.u08 => q"$ref.jsonPrimitive.int.toUByte()" + case TypeId.Builtins.u16 => q"$ref.jsonPrimitive.int.toUShort()" + case TypeId.Builtins.u32 => q"$ref.jsonPrimitive.long.toUInt()" + case TypeId.Builtins.u64 => q"$ref.jsonPrimitive.long.toULong()" + case TypeId.Builtins.f32 => q"$ref.jsonPrimitive.float" + case TypeId.Builtins.f64 => q"$ref.jsonPrimitive.double" + case TypeId.Builtins.f128 => + if (ktTypes.multiplatform) q"${ktTypes.ktBigDecimal}.fromString($ref.jsonPrimitive.content)" else q"java.math.BigDecimal($ref.jsonPrimitive.content)" case TypeId.Builtins.str => q"$ref.jsonPrimitive.content" case TypeId.Builtins.bytes => q"$ktByteString.fromHexString($ref.jsonPrimitive.content)" - case TypeId.Builtins.uid => if (ktTypes.multiplatform) q"kotlin.uuid.Uuid.parse($ref.jsonPrimitive.content)" else q"java.util.UUID.fromString($ref.jsonPrimitive.content)" - case TypeId.Builtins.tsu => q"$baboonTimeFormats.parseTsu($ref.jsonPrimitive.content)" - case TypeId.Builtins.tso => q"$baboonTimeFormats.parseTso($ref.jsonPrimitive.content)" + case TypeId.Builtins.uid => + if (ktTypes.multiplatform) q"kotlin.uuid.Uuid.parse($ref.jsonPrimitive.content)" else q"java.util.UUID.fromString($ref.jsonPrimitive.content)" + case TypeId.Builtins.tsu => q"$baboonTimeFormats.parseTsu($ref.jsonPrimitive.content)" + case TypeId.Builtins.tso => q"$baboonTimeFormats.parseTso($ref.jsonPrimitive.content)" case u: TypeId.User => domain.defs.meta.nodes(u) match { case DomainMember.User(_, f: Typedef.Foreign, _, _) => diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtServiceWiringTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtServiceWiringTranslator.scala index e833360c..11dd8143 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtServiceWiringTranslator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtServiceWiringTranslator.scala @@ -33,18 +33,22 @@ object KtServiceWiringTranslator { ServiceContextResolver.resolve(domain, "kotlin", target.language.serviceContext, target.language.pragmas) private def hasActiveJsonCodecs(service: Typedef.Service): Boolean = { - codecs.exists { c => - c.isInstanceOf[KtJsonCodecGenerator] && service.methods.forall { m => - c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) - } + codecs.exists { + c => + c.isInstanceOf[KtJsonCodecGenerator] && service.methods.forall { + m => + c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) + } } } private def hasActiveUebaCodecs(service: Typedef.Service): Boolean = { - codecs.exists { c => - c.isInstanceOf[KtUEBACodecGenerator] && service.methods.forall { m => - c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) - } + codecs.exists { + c => + c.isInstanceOf[KtUEBACodecGenerator] && service.methods.forall { + m => + c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) + } } } @@ -553,53 +557,54 @@ object KtServiceWiringTranslator { override def translateClient(defn: DomainMember.User): Option[TextTree[KtValue]] = { defn.defn match { case service: Typedef.Service => - val svcName = service.id.name.name - val hasUeba = hasActiveUebaCodecs(service) - val hasJson = hasActiveJsonCodecs(service) + val svcName = service.id.name.name + val hasUeba = hasActiveUebaCodecs(service) + val hasJson = hasActiveJsonCodecs(service) if (!hasUeba && !hasJson) return None - val clientMethods = service.methods.flatMap { m => - val inTypeId = m.sig.id.asInstanceOf[TypeId.User] - val inTypeRef = trans.asKtRef(m.sig, domain, evo) - val outTypeRef = m.out.map(t => trans.asKtRef(t, domain, evo)) - val retType = outTypeRef.getOrElse(q"Unit") - - val uebaMethod = if (hasUeba) { - val inCodec = uebaCodecName(inTypeId) - val decodeOut = m.out match { - case Some(outRef) => - val outCodec = uebaCodecName(outRef.id.asInstanceOf[TypeId.User]) - q"return $outCodec.instance.decode(ctx, ${mkInlineReader("resp")})" - case None => q"return Unit as $retType" - } - Some( - q"""suspend fun ${m.name.name}(arg: $inTypeRef, ctx: $baboonCodecContext = $baboonCodecContext.Default): $retType { - | ${mkWriterSetup("writer").shift(2).trim} - | $inCodec.instance.encode(ctx, writer, arg) - | val resp = transportUeba("$svcName", "${m.name.name}", ${mkWriterGetBytes("writer")}) - | ${decodeOut.shift(2).trim} - |}""".stripMargin - ) - } else None - - val jsonMethod = if (hasJson) { - val inCodec = jsonCodecName(inTypeId) - val decodeOut = m.out match { - case Some(outRef) => - val outCodec = jsonCodecName(outRef.id.asInstanceOf[TypeId.User]) - q"return $outCodec.decode($baboonCodecContext.Default, $kotlinxJson.parseToJsonElement(resp))" - case None => q"return Unit as $retType" - } - Some( - q"""suspend fun ${m.name.name}Json(arg: $inTypeRef): $retType { - | val encoded = $inCodec.encode($baboonCodecContext.Default, arg).toString() - | val resp = transportJson("$svcName", "${m.name.name}", encoded) - | ${decodeOut.shift(2).trim} - |}""".stripMargin - ) - } else None - - uebaMethod.toList ++ jsonMethod.toList + val clientMethods = service.methods.flatMap { + m => + val inTypeId = m.sig.id.asInstanceOf[TypeId.User] + val inTypeRef = trans.asKtRef(m.sig, domain, evo) + val outTypeRef = m.out.map(t => trans.asKtRef(t, domain, evo)) + val retType = outTypeRef.getOrElse(q"Unit") + + val uebaMethod = if (hasUeba) { + val inCodec = uebaCodecName(inTypeId) + val decodeOut = m.out match { + case Some(outRef) => + val outCodec = uebaCodecName(outRef.id.asInstanceOf[TypeId.User]) + q"return $outCodec.instance.decode(ctx, ${mkInlineReader("resp")})" + case None => q"return Unit as $retType" + } + Some( + q"""suspend fun ${m.name.name}(arg: $inTypeRef, ctx: $baboonCodecContext = $baboonCodecContext.Default): $retType { + | ${mkWriterSetup("writer").shift(2).trim} + | $inCodec.instance.encode(ctx, writer, arg) + | val resp = transportUeba("$svcName", "${m.name.name}", ${mkWriterGetBytes("writer")}) + | ${decodeOut.shift(2).trim} + |}""".stripMargin + ) + } else None + + val jsonMethod = if (hasJson) { + val inCodec = jsonCodecName(inTypeId) + val decodeOut = m.out match { + case Some(outRef) => + val outCodec = jsonCodecName(outRef.id.asInstanceOf[TypeId.User]) + q"return $outCodec.decode($baboonCodecContext.Default, $kotlinxJson.parseToJsonElement(resp))" + case None => q"return Unit as $retType" + } + Some( + q"""suspend fun ${m.name.name}Json(arg: $inTypeRef): $retType { + | val encoded = $inCodec.encode($baboonCodecContext.Default, arg).toString() + | val resp = transportJson("$svcName", "${m.name.name}", encoded) + | ${decodeOut.shift(2).trim} + |}""".stripMargin + ) + } else None + + uebaMethod.toList ++ jsonMethod.toList } if (clientMethods.isEmpty) return None diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtTypeTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtTypeTranslator.scala index 9f267c23..d884813a 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtTypeTranslator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtTypeTranslator.scala @@ -51,23 +51,23 @@ class KtTypeTranslator(ktTypes: KtTypes) { tpe match { case b: TypeId.BuiltinScalar => b match { - case TypeId.Builtins.i08 => ktByte - case TypeId.Builtins.u08 => ktUByte - case TypeId.Builtins.i16 => ktShort - case TypeId.Builtins.u16 => ktUShort - case TypeId.Builtins.i32 => ktInt - case TypeId.Builtins.u32 => ktUInt - case TypeId.Builtins.i64 => ktLong - case TypeId.Builtins.u64 => ktULong - case TypeId.Builtins.f32 => ktFloat - case TypeId.Builtins.f64 => ktDouble - case TypeId.Builtins.f128 => ktBigDecimal - case TypeId.Builtins.str => ktString - case TypeId.Builtins.bytes => ktByteString - case TypeId.Builtins.uid => ktUid - case TypeId.Builtins.tsu => ktTimeTsu - case TypeId.Builtins.tso => ktTimeTso - case TypeId.Builtins.bit => ktBoolean + case TypeId.Builtins.i08 => ktByte + case TypeId.Builtins.u08 => ktUByte + case TypeId.Builtins.i16 => ktShort + case TypeId.Builtins.u16 => ktUShort + case TypeId.Builtins.i32 => ktInt + case TypeId.Builtins.u32 => ktUInt + case TypeId.Builtins.i64 => ktLong + case TypeId.Builtins.u64 => ktULong + case TypeId.Builtins.f32 => ktFloat + case TypeId.Builtins.f64 => ktDouble + case TypeId.Builtins.f128 => ktBigDecimal + case TypeId.Builtins.str => ktString + case TypeId.Builtins.bytes => ktByteString + case TypeId.Builtins.uid => ktUid + case TypeId.Builtins.tsu => ktTimeTsu + case TypeId.Builtins.tso => ktTimeTso + case TypeId.Builtins.bit => ktBoolean case other => throw new IllegalArgumentException(s"Unexpected: $other") } diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtTypes.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtTypes.scala index 5dcb3287..c2deb699 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtTypes.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtTypes.scala @@ -155,8 +155,8 @@ object KtTypes { val javaMathPkg: KtPackageId = parseKtPkg("java.math") val javaTimePkg: KtPackageId = parseKtPkg("java.time") - val javaIoPkg: KtPackageId = parseKtPkg("java.io") - val javaFile: KtType = KtType(javaIoPkg, "File") + val javaIoPkg: KtPackageId = parseKtPkg("java.io") + val javaFile: KtType = KtType(javaIoPkg, "File") val javaNioFilePkg: KtPackageId = parseKtPkg("java.nio.file") val javaNioFiles: KtType = KtType(javaNioFilePkg, "Files") diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtUEBACodecGenerator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtUEBACodecGenerator.scala index dec39bfd..ffc3d62a 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtUEBACodecGenerator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/kotlin/KtUEBACodecGenerator.scala @@ -466,18 +466,19 @@ class KtUEBACodecGenerator( id match { case s: TypeId.BuiltinScalar => s match { - case TypeId.Builtins.bit => q"$wref.writeBoolean($ref)" - case TypeId.Builtins.i08 => q"$wref.writeByte($ref.toInt())" - case TypeId.Builtins.i16 => q"$wref.writeShort($ref.toInt())" - case TypeId.Builtins.i32 => q"$wref.writeInt($ref)" - case TypeId.Builtins.i64 => q"$wref.writeLong($ref)" - case TypeId.Builtins.u08 => q"$wref.writeByte($ref.toInt())" - case TypeId.Builtins.u16 => q"$wref.writeShort($ref.toInt())" - case TypeId.Builtins.u32 => q"$wref.writeInt($ref.toInt())" - case TypeId.Builtins.u64 => q"$wref.writeLong($ref.toLong())" - case TypeId.Builtins.f32 => q"$wref.writeFloat($ref)" - case TypeId.Builtins.f64 => q"$wref.writeDouble($ref)" - case TypeId.Builtins.f128 => if (ktTypes.multiplatform) q"$baboonBinTools.writeBaboonDecimal($wref, $ref)" else q"$baboonBinTools.writeBigDecimal($wref, $ref)" + case TypeId.Builtins.bit => q"$wref.writeBoolean($ref)" + case TypeId.Builtins.i08 => q"$wref.writeByte($ref.toInt())" + case TypeId.Builtins.i16 => q"$wref.writeShort($ref.toInt())" + case TypeId.Builtins.i32 => q"$wref.writeInt($ref)" + case TypeId.Builtins.i64 => q"$wref.writeLong($ref)" + case TypeId.Builtins.u08 => q"$wref.writeByte($ref.toInt())" + case TypeId.Builtins.u16 => q"$wref.writeShort($ref.toInt())" + case TypeId.Builtins.u32 => q"$wref.writeInt($ref.toInt())" + case TypeId.Builtins.u64 => q"$wref.writeLong($ref.toLong())" + case TypeId.Builtins.f32 => q"$wref.writeFloat($ref)" + case TypeId.Builtins.f64 => q"$wref.writeDouble($ref)" + case TypeId.Builtins.f128 => + if (ktTypes.multiplatform) q"$baboonBinTools.writeBaboonDecimal($wref, $ref)" else q"$baboonBinTools.writeBigDecimal($wref, $ref)" case TypeId.Builtins.str => q"$baboonBinTools.writeString($wref, $ref)" case TypeId.Builtins.bytes => q"$baboonBinTools.writeByteString($wref, $ref)" case TypeId.Builtins.uid => q"$baboonBinTools.writeUid($wref, $ref)" diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/openapi/OasBaboonTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/openapi/OasBaboonTranslator.scala new file mode 100644 index 00000000..8552e75f --- /dev/null +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/openapi/OasBaboonTranslator.scala @@ -0,0 +1,242 @@ +package io.septimalmind.baboon.translator.openapi + +import io.septimalmind.baboon.CompilerProduct +import io.septimalmind.baboon.CompilerTarget.OasTarget +import io.septimalmind.baboon.parser.model.issues.BaboonIssue +import io.septimalmind.baboon.translator.{BaboonAbstractTranslator, OutputFile, Sources} +import io.septimalmind.baboon.typer.model.* +import izumi.functional.bio.{Error2, F} +import izumi.fundamentals.collections.IzCollections.* +import izumi.fundamentals.collections.nonempty.NEList +import io.septimalmind.baboon.parser.model.issues.TranslationIssue + +/** Translates Baboon domain models to OpenAPI 3.1 component schemas. + * + * Emits one `openapi.json` file per domain version containing an OpenAPI 3.1 + * document with all type definitions under `components/schemas`. Schema-only -- + * no paths, codecs, conversions, or runtime are generated. + * + * === Type mapping conventions === + * + * - '''DTO''' -> JSON Schema `object` with `properties` and `required` + * - '''Enum''' -> JSON Schema `string` with `enum` keyword + * - '''ADT''' -> `oneOf` referencing each branch schema + * - '''Foreign with `rt`''' -> resolved to the underlying Baboon type + * - '''Foreign without `rt`''' -> opaque `object` with a description + * - '''Service / Contract''' -> skipped (non-data types) + * - '''Type aliases''' -> transparent, resolved by the typer before we see them + * + * === Scalar mapping (Baboon -> JSON Schema type/format) === + * + * - `bit` -> `boolean` + * - `str` -> `string` + * - `i08`/`i16`/`i32` -> `integer` / `int32` + * - `i64` -> `integer` / `int64` + * - `u08`/`u16`/`u32` -> `integer` / `int32` with `minimum: 0` + * - `u64` -> `integer` / `int64` with `minimum: 0` + * - `f32` -> `number` / `float` + * - `f64` -> `number` / `double` + * - `f128` -> `string` / `decimal` (no native 128-bit float in JSON) + * - `uid` -> `string` / `uuid` + * - `tsu` / `tso` -> `string` / `date-time` + * - `bytes` -> `string` / `byte` (base64-encoded) + * + * === Collection mapping === + * + * - `opt[T]` -> `oneOf: [T, {type: "null"}]` (JSON Schema 2020-12 nullable) + * - `lst[T]` -> `{type: "array", items: T}` + * - `set[T]` -> `{type: "array", items: T, uniqueItems: true}` + * - `map[str, V]` -> `{type: "object", additionalProperties: V}` + * - `map[K, V]` (non-string key) -> `{type: "array", items: {type: "object", + * properties: {key: K, value: V}}}` -- JSON has no native map-with-non-string-keys, + * so we fall back to an array of key-value entry objects. + * + * === Limitations === + * + * - `paths` is always empty -- the output is a component-schema library, not a + * full API specification. Consumers should `$ref` into `components/schemas`. + * - No discriminator `mapping` is emitted for ADTs because branch schemas are + * always in the same document; consumers can rely on `oneOf` + schema structure. + * - Unsigned integer constraints (`minimum: 0`) are advisory -- JSON Schema + * validators enforce them, but some OpenAPI tooling may ignore `minimum` on + * integer schemas. + * - `f128` is represented as a string since JSON numbers cannot carry 128-bit + * precision; consumers must parse the string value themselves. + * - `tsu` and `tso` both map to `date-time`; the distinction between UTC and + * offset timestamps is lost at the schema level. + * - Empty DTOs produce `{type: "object"}` with no properties. + */ +class OasBaboonTranslator[F[+_, +_]: Error2]( + target: OasTarget, + typeTranslator: OasTypeTranslator, +) extends BaboonAbstractTranslator[F] { + + override def translate(family: BaboonFamily): F[NEList[BaboonIssue], Sources] = { + for { + rendered <- F.fromEither { + val allFiles = family.domains.iterator.flatMap { + case (_, lineage) => + lineage.versions.iterator.map { + case (_, domain) => translateDomain(domain) + } + }.toList + + val flattened = allFiles.flatMap(identity) + + flattened.toUniqueMap(c => + BaboonIssue.of(TranslationIssue.NonUniqueOutputFiles(c)) + ) + } + } yield Sources(rendered) + } + + private def translateDomain(domain: Domain): List[(String, OutputFile)] = { + if (!target.output.products.contains(CompilerProduct.Definition)) { + return List.empty + } + + val pkg = domain.id + val ver = domain.version + val foreignResolutions = typeTranslator.foreignTypeResolution(domain) + + val members = domain.defs.meta.nodes.values.collect { + case u: DomainMember.User => u + }.toList.sortBy(_.id.toString) + + val schemas = members.flatMap { m => + m.defn match { + case _: Typedef.NonDataTypedef => None + case f: Typedef.Foreign if f.runtimeMapping.nonEmpty => None + case f: Typedef.Foreign => Some(renderForeignSchema(f)) + case _ if m.ownedByAdt => None + case defn => Some(renderTypedef(defn, domain, foreignResolutions)) + } + } + + val schemasJson = schemas.mkString(",\n") + + val esc = typeTranslator.escapeJson _ + val doc = + s"""{ + | "openapi": "3.1.0", + | "info": { + | "title": "${esc(pkg.toString)}", + | "version": "${esc(ver.toString)}" + | }, + | "paths": {}, + | "components": { + | "schemas": { + |$schemasJson + | } + | } + |} + |""".stripMargin + + val versionStr = ver.toString.replace(".", "_") + val pkgStr = pkg.path.toList.map(typeTranslator.sanitize).mkString("_") + val filename = s"$pkgStr/v$versionStr/openapi.json" + + List(filename -> OutputFile(doc, CompilerProduct.Definition)) + } + + private def renderTypedef( + defn: Typedef.User, + domain: Domain, + foreignResolutions: Map[TypeId.User, Option[TypeRef]], + ): String = { + defn match { + case dto: Typedef.Dto => renderDto(dto, foreignResolutions) + case e: Typedef.Enum => renderEnum(e) + case adt: Typedef.Adt => renderAdt(adt, domain, foreignResolutions) + case other => throw new IllegalArgumentException(s"Unexpected typedef in OpenAPI renderTypedef: ${other.id}") + } + } + + private def renderDto(dto: Typedef.Dto, foreignResolutions: Map[TypeId.User, Option[TypeRef]]): String = { + val name = typeTranslator.schemaName(dto.id) + val esc = typeTranslator.escapeJson _ + + if (dto.fields.isEmpty) { + s""" "${esc(name)}": {"type": "object"}""" + } else { + val resolvedFields = dto.fields.map(f => f.copy(tpe = typeTranslator.resolveTypeRef(f.tpe, foreignResolutions))) + + val requiredFields = resolvedFields.filterNot(f => isOptional(f.tpe)) + val requiredJson = + if (requiredFields.isEmpty) "" + else { + val names = requiredFields.map(f => s""""${esc(f.name.name)}"""").mkString(", ") + s""", "required": [$names]""" + } + + val propsJson = resolvedFields.map { f => + val schema = typeTranslator.typeRefSchema(f.tpe) + s""" "${esc(f.name.name)}": $schema""" + }.mkString(",\n") + + s""" "${esc(name)}": {"type": "object"$requiredJson, "properties": { + |$propsJson + | }}""".stripMargin + } + } + + private def renderEnum(e: Typedef.Enum): String = { + val name = typeTranslator.schemaName(e.id) + val esc = typeTranslator.escapeJson _ + val values = e.members.toList.map(m => s""""${esc(m.name)}"""").mkString(", ") + s""" "${esc(name)}": {"type": "string", "enum": [$values]}""" + } + + private def renderAdt( + adt: Typedef.Adt, + domain: Domain, + foreignResolutions: Map[TypeId.User, Option[TypeRef]], + ): String = { + import Typedef.Adt.AdtSyntax + val name = typeTranslator.schemaName(adt.id) + val esc = typeTranslator.escapeJson _ + val dataMembers = adt.dataMembers(domain) + + // Emit each branch schema inline, then the union + val branchSchemas = dataMembers.flatMap { memberId => + domain.defs.meta.nodes.get(memberId).collect { + case u: DomainMember.User => + u.defn match { + case dto: Typedef.Dto => renderDto(dto, foreignResolutions) + case e: Typedef.Enum => renderEnum(e) + case other => throw new IllegalArgumentException(s"Unexpected ADT branch type in OpenAPI backend: ${other.id}") + } + } + } + + val branchRefs = dataMembers.map { memberId => + val refName = typeTranslator.schemaName(memberId) + s"""{"$$ref": "#/components/schemas/${esc(refName)}"}""" + } + + val refsJson = branchRefs.mkString(", ") + val adtSchema = s""" "${esc(name)}": {"oneOf": [$refsJson]}""" + + if (branchSchemas.nonEmpty) { + val branchLines = branchSchemas.mkString(",\n") + s"""$branchLines, + |$adtSchema""".stripMargin + } else { + adtSchema + } + } + + private def renderForeignSchema(f: Typedef.Foreign): String = { + val name = typeTranslator.schemaName(f.id) + val esc = typeTranslator.escapeJson _ + s""" "${esc(name)}": {"type": "object", "description": "Foreign type: ${esc(name)}"}""" + } + + private def isOptional(ref: TypeRef): Boolean = { + ref match { + case TypeRef.Constructor(TypeId.Builtins.opt, _) => true + case _ => false + } + } + +} diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/openapi/OasTypeTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/openapi/OasTypeTranslator.scala new file mode 100644 index 00000000..c3db424c --- /dev/null +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/openapi/OasTypeTranslator.scala @@ -0,0 +1,148 @@ +package io.septimalmind.baboon.translator.openapi + +import io.septimalmind.baboon.typer.model.* + +class OasTypeTranslator { + + def foreignTypeResolution(domain: Domain): Map[TypeId.User, Option[TypeRef]] = { + domain.defs.meta.nodes.values.collect { + case u: DomainMember.User => + u.defn match { + case f: Typedef.Foreign => Some(f.id -> f.runtimeMapping) + case _ => None + } + }.flatten.toMap + } + + def resolveTypeRef(ref: TypeRef, foreignResolutions: Map[TypeId.User, Option[TypeRef]]): TypeRef = { + ref match { + case TypeRef.Scalar(id: TypeId.User) => + foreignResolutions.get(id) match { + case Some(Some(resolved)) => resolveTypeRef(resolved, foreignResolutions) + case _ => ref + } + case TypeRef.Constructor(id, args) => + TypeRef.Constructor(id, args.map(a => resolveTypeRef(a, foreignResolutions))) + case _ => ref + } + } + + /** JSON Schema representation of a scalar Baboon type. + * + * Returns `(type, format, extra)` where `extra` may contain additional + * properties like `"minimum": 0` for unsigned integers. + */ + def scalarSchema(id: TypeId.BuiltinScalar): (String, Option[String], Map[String, String]) = { + id match { + case TypeId.Builtins.bit => ("boolean", None, Map.empty) + case TypeId.Builtins.str => ("string", None, Map.empty) + case TypeId.Builtins.i08 => ("integer", Some("int32"), Map.empty) + case TypeId.Builtins.i16 => ("integer", Some("int32"), Map.empty) + case TypeId.Builtins.i32 => ("integer", Some("int32"), Map.empty) + case TypeId.Builtins.i64 => ("integer", Some("int64"), Map.empty) + case TypeId.Builtins.u08 => ("integer", Some("int32"), Map("minimum" -> "0")) + case TypeId.Builtins.u16 => ("integer", Some("int32"), Map("minimum" -> "0")) + case TypeId.Builtins.u32 => ("integer", Some("int32"), Map("minimum" -> "0")) + case TypeId.Builtins.u64 => ("integer", Some("int64"), Map("minimum" -> "0")) + case TypeId.Builtins.f32 => ("number", Some("float"), Map.empty) + case TypeId.Builtins.f64 => ("number", Some("double"), Map.empty) + case TypeId.Builtins.f128 => ("string", Some("decimal"), Map.empty) + case TypeId.Builtins.uid => ("string", Some("uuid"), Map.empty) + case TypeId.Builtins.tsu => ("string", Some("date-time"), Map.empty) + case TypeId.Builtins.tso => ("string", Some("date-time"), Map.empty) + case TypeId.Builtins.bytes => ("string", Some("byte"), Map.empty) + case other => throw new IllegalArgumentException(s"Unexpected builtin scalar in OpenAPI backend: ${other.name.name}") + } + } + + /** Inline JSON Schema fragment for a type reference. + * + * Returns a JSON string (without surrounding braces or commas) that can be + * embedded as a property schema or array items schema. + */ + def typeRefSchema(ref: TypeRef): String = { + ref match { + case TypeRef.Scalar(id: TypeId.BuiltinScalar) => + scalarSchemaJson(id) + + case TypeRef.Scalar(id: TypeId.User) => + s"""{"$$ref": "#/components/schemas/${escapeJson(schemaName(id))}"}""" + + case TypeRef.Constructor(TypeId.Builtins.opt, args) => + // nullable via oneOf [schema, null] (OpenAPI 3.1 / JSON Schema 2020-12) + val inner = typeRefSchema(args.head) + s"""{"oneOf": [$inner, {"type": "null"}]}""" + + case TypeRef.Constructor(TypeId.Builtins.lst, args) => + val items = typeRefSchema(args.head) + s"""{"type": "array", "items": $items}""" + + case TypeRef.Constructor(TypeId.Builtins.set, args) => + val items = typeRefSchema(args.head) + s"""{"type": "array", "items": $items, "uniqueItems": true}""" + + case TypeRef.Constructor(TypeId.Builtins.map, args) => + mapSchema(args.head, args.tail.head) + case other => + throw new IllegalArgumentException(s"Unexpected type reference in OpenAPI backend: ${other.id.name.name}") + } + } + + /** JSON Schema for a map type. + * + * String-keyed maps become `{"type": "object", "additionalProperties": ...}`. + * Non-string-keyed maps become arrays of `{key, value}` entry objects. + */ + private def mapSchema(keyRef: TypeRef, valRef: TypeRef): String = { + val valSchema = typeRefSchema(valRef) + if (isStringKey(keyRef)) { + s"""{"type": "object", "additionalProperties": $valSchema}""" + } else { + val keySchema = typeRefSchema(keyRef) + val entrySchema = + s"""{"type": "object", "required": ["key", "value"], "properties": {"key": $keySchema, "value": $valSchema}}""" + s"""{"type": "array", "items": $entrySchema}""" + } + } + + private def isStringKey(ref: TypeRef): Boolean = { + ref match { + case TypeRef.Scalar(TypeId.Builtins.str) => true + case TypeRef.Scalar(TypeId.Builtins.uid) => true + case _ => false + } + } + + def scalarSchemaJson(id: TypeId.BuiltinScalar): String = { + val (tpe, fmt, extra) = scalarSchema(id) + val parts = List(s""""type": "$tpe"""") ++ + fmt.map(f => s""""format": "$f"""").toList ++ + extra.map { case (k, v) => s""""$k": $v""" } + s"{${parts.mkString(", ")}}" + } + + /** Generate the schema name for a user-defined type, following the same + * conventions as the GraphQL backend: package path + owner path + type name, + * joined with underscores. + */ + def schemaName(id: TypeId.User): String = { + val parts = id.pkg.path.toList ++ id.owner.asPseudoPkg :+ id.name.name + parts.map(sanitize).mkString("_") + } + + def sanitize(s: String): String = { + s.replace("-", "_").replace(".", "_") + } + + def escapeJson(s: String): String = { + s.flatMap { + case '"' => "\\\"" + case '\\' => "\\\\" + case '\n' => "\\n" + case '\r' => "\\r" + case '\t' => "\\t" + case c if c < 0x20 => f"\\u${c.toInt}%04x" + case c => c.toString + } + } +} diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/python/PyBaboonTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/python/PyBaboonTranslator.scala index 60317a7a..3fdbc7d9 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/python/PyBaboonTranslator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/python/PyBaboonTranslator.scala @@ -180,8 +180,9 @@ class PyBaboonTranslator[F[+_, +_]: Error2]( val usualImportsByModule = usual.groupBy(_.moduleId).toList.sortBy { case (moduleId, types) => moduleId.path.size + types.size }.reverse.map { case (module, types) => - val typesString = types.map { t => - aliasMap.get(t).map(a => s"${t.name} as $a").getOrElse(t.name) + val typesString = types.map { + t => + aliasMap.get(t).map(a => s"${t.name} as $a").getOrElse(t.name) }.mkString(", ") if (module == pyBaboonCodecsModule || module == pyBaboonSharedRuntimeModule || module == pyBaboonConversionsModule || module == pyBaboonServiceWiringModule) { val baseString = pyFileTools.definitionsBasePkg.mkString(".") @@ -204,13 +205,14 @@ class PyBaboonTranslator[F[+_, +_]: Error2]( val conflicting = usedTypes.groupBy(_.name).filter(_._2.size > 1) conflicting.flatMap { case (name, group) => - val paths = group.map(t => t -> t.moduleId.path.toList) - val commonLen = paths.map(_._2).reduce((a, b) => a.zip(b).takeWhile { case (x, y) => x == y }.map(_._1)).size + val paths = group.map(t => t -> t.moduleId.path.toList) + val commonLen = paths.map(_._2).reduce((a, b) => a.zip(b).takeWhile { case (x, y) => x == y }.map(_._1)).size paths.map { case (t, path) => val distinguishing = path.drop(commonLen).dropRight(1) - val prefix = if (distinguishing.nonEmpty) distinguishing.mkString("_") - else path.dropRight(1).lastOption.getOrElse("m") + val prefix = + if (distinguishing.nonEmpty) distinguishing.mkString("_") + else path.dropRight(1).lastOption.getOrElse("m") t -> s"${prefix}_$name" } } diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/python/PyServiceWiringTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/python/PyServiceWiringTranslator.scala index 88611878..e91bc22e 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/python/PyServiceWiringTranslator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/python/PyServiceWiringTranslator.scala @@ -32,18 +32,22 @@ object PyServiceWiringTranslator { ServiceContextResolver.resolve(domain, "python", target.language.serviceContext, target.language.pragmas) private def hasActiveJsonCodecs(service: Typedef.Service): Boolean = { - codecs.exists { c => - c.isInstanceOf[PyJsonCodecGenerator] && service.methods.forall { m => - c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) - } + codecs.exists { + c => + c.isInstanceOf[PyJsonCodecGenerator] && service.methods.forall { + m => + c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) + } } } private def hasActiveUebaCodecs(service: Typedef.Service): Boolean = { - codecs.exists { c => - c.isInstanceOf[PyUEBACodecGenerator] && service.methods.forall { m => - c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) - } + codecs.exists { + c => + c.isInstanceOf[PyUEBACodecGenerator] && service.methods.forall { + m => + c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) + } } } diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/rust/RsBaboonTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/rust/RsBaboonTranslator.scala index 5812e8d5..a1286c0c 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/rust/RsBaboonTranslator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/rust/RsBaboonTranslator.scala @@ -178,7 +178,7 @@ class RsBaboonTranslator[F[+_, +_]: Error2]( List(q"pub mod $escaped;") } else { val reexport = target.language.reexportMode match { - case "none" => false + case "none" => false case "selective" => // Only re-export if not a wiring, client, conversion, or test module // Note: _fixture must be re-exported because generated tests and cross-fixture references use super:: @@ -348,8 +348,8 @@ class RsBaboonTranslator[F[+_, +_]: Error2]( case TypeId.Builtins.tsu | TypeId.Builtins.tso => true case _ => false } - val hasUuids = allTypes.contains(TypeId.Builtins.uid) - val hasDecimals = allTypes.contains(TypeId.Builtins.f128) + val hasUuids = allTypes.contains(TypeId.Builtins.uid) + val hasDecimals = allTypes.contains(TypeId.Builtins.f128) val hasJsonCodecs = target.language.generateJsonCodecs // lenient_numeric (requires serde_json) is used whenever any field contains i64/u64 val hasLenientNumeric = allTypes.exists { diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/rust/RsConversionTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/rust/RsConversionTranslator.scala index a6958d69..14ee292f 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/rust/RsConversionTranslator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/rust/RsConversionTranslator.scala @@ -2,7 +2,7 @@ package io.septimalmind.baboon.translator.rust import distage.Id import io.septimalmind.baboon.parser.model.issues.{BaboonIssue, TranslationIssue} -import io.septimalmind.baboon.translator.rust.RsDefnTranslator.{toSnakeCase, toSnakeCaseFileName, escapeRustModuleName} +import io.septimalmind.baboon.translator.rust.RsDefnTranslator.{escapeRustModuleName, toSnakeCase, toSnakeCaseFileName} import io.septimalmind.baboon.translator.rust.RsValue.RsCrateId import io.septimalmind.baboon.typer.model.* import io.septimalmind.baboon.typer.model.Conversion.FieldOp @@ -137,11 +137,12 @@ class RsConversionTranslator[F[+_, +_]: Error2]( case _ => throw new IllegalStateException("DTO expected") } val ops = c.ops.map(o => o.targetField -> o).toMap - val usesFrom = dto.fields.exists { f => - ops(f) match { - case _: FieldOp.InitializeWithDefault => false - case _ => true - } + val usesFrom = dto.fields.exists { + f => + ops(f) match { + case _: FieldOp.InitializeWithDefault => false + case _ => true + } } val fromParam = if (usesFrom) "from" else "_from" val assigns = dto.fields.map { diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/rust/RsDefnTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/rust/RsDefnTranslator.scala index 4127984d..099f2d28 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/rust/RsDefnTranslator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/rust/RsDefnTranslator.scala @@ -313,8 +313,8 @@ object RsDefnTranslator { tpe match { case TypeRef.Scalar(TypeId.Builtins.f32) => true case TypeRef.Scalar(TypeId.Builtins.f64) => true - case TypeRef.Constructor(_, args) => args.exists(hasDirectFloat) - case _ => false + case TypeRef.Constructor(_, args) => args.exists(hasDirectFloat) + case _ => false } } @@ -500,7 +500,7 @@ object RsDefnTranslator { q""""$branchName" => Ok(${name.asName}::$branchName(map.next_value()?)),""" } - val branchNames = dataMembers.map(_.name.name.capitalize) + val branchNames = dataMembers.map(_.name.name.capitalize) val branchNamesLit = branchNames.map(n => s""""$n"""").mkString(", ") val displayBranches = dataMembers.map { @@ -583,11 +583,11 @@ object RsDefnTranslator { case t: RsValue.RsType => if (t.predef) t.name else (t.crate.parts :+ t.name).mkString("::") case t: RsValue.RsTypeName => t.name } - val inStr = inType.mapRender(rsFqName) - val outStr = outType.map(_.mapRender(rsFqName)).getOrElse("") - val errStr = errType.map(_.mapRender(rsFqName)) - val retStr = resolved.renderReturnType(outStr, errStr, "()") - val asyncKw = if (target.language.asyncServices) "async " else "" + val inStr = inType.mapRender(rsFqName) + val outStr = outType.map(_.mapRender(rsFqName)).getOrElse("") + val errStr = errType.map(_.mapRender(rsFqName)) + val retStr = resolved.renderReturnType(outStr, errStr, "()") + val asyncKw = if (target.language.asyncServices) "async " else "" q"${asyncKw}fn ${toSnakeCase(m.name.name)}(&self, ${ctxParam}arg: $inStr) -> $retStr;" } val genericParam = resolvedCtx match { @@ -618,9 +618,43 @@ object RsDefnTranslator { } private val rustKeywords: Set[String] = Set( - "type", "self", "super", "crate", "mod", "fn", "let", "mut", "ref", "match", "if", "else", "while", "for", "loop", "break", "continue", - "return", "struct", "enum", "trait", "impl", "use", "pub", "as", "in", "where", "async", "await", "dyn", "move", "static", "const", "unsafe", - "extern", "true", "false", + "type", + "self", + "super", + "crate", + "mod", + "fn", + "let", + "mut", + "ref", + "match", + "if", + "else", + "while", + "for", + "loop", + "break", + "continue", + "return", + "struct", + "enum", + "trait", + "impl", + "use", + "pub", + "as", + "in", + "where", + "async", + "await", + "dyn", + "move", + "static", + "const", + "unsafe", + "extern", + "true", + "false", ) def isRustKeyword(s: String): Boolean = rustKeywords.contains(s) @@ -634,9 +668,9 @@ object RsDefnTranslator { */ def escapeRustModuleName(s: String): String = { s match { - case "in" => "input" + case "in" => "input" case kw if isRustKeyword(kw) => s"${kw}_" - case other => other + case other => other } } diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/rust/RsServiceWiringTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/rust/RsServiceWiringTranslator.scala index 1c8ccfe6..3c0d6440 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/rust/RsServiceWiringTranslator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/rust/RsServiceWiringTranslator.scala @@ -34,18 +34,22 @@ object RsServiceWiringTranslator { ServiceContextResolver.resolve(domain, "rust", target.language.serviceContext, target.language.pragmas) private def hasActiveJsonCodecs(service: Typedef.Service): Boolean = { - codecs.exists { c => - c.isInstanceOf[RsJsonCodecGenerator] && service.methods.forall { m => - c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) - } + codecs.exists { + c => + c.isInstanceOf[RsJsonCodecGenerator] && service.methods.forall { + m => + c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) + } } } private def hasActiveUebaCodecs(service: Typedef.Service): Boolean = { - codecs.exists { c => - c.isInstanceOf[RsUEBACodecGenerator] && service.methods.forall { m => - c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) - } + codecs.exists { + c => + c.isInstanceOf[RsUEBACodecGenerator] && service.methods.forall { + m => + c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) + } } } @@ -170,11 +174,11 @@ object RsServiceWiringTranslator { if (clientMethods.isEmpty) return None // Build type params and fields - val typeParams = scala.collection.mutable.ListBuffer.empty[String] - val whereClauses = scala.collection.mutable.ListBuffer.empty[String] - val fields = scala.collection.mutable.ListBuffer.empty[TextTree[RsValue]] - val ctorParams = scala.collection.mutable.ListBuffer.empty[TextTree[RsValue]] - val ctorAssigns = scala.collection.mutable.ListBuffer.empty[TextTree[RsValue]] + val typeParams = scala.collection.mutable.ListBuffer.empty[String] + val whereClauses = scala.collection.mutable.ListBuffer.empty[String] + val fields = scala.collection.mutable.ListBuffer.empty[TextTree[RsValue]] + val ctorParams = scala.collection.mutable.ListBuffer.empty[TextTree[RsValue]] + val ctorAssigns = scala.collection.mutable.ListBuffer.empty[TextTree[RsValue]] if (hasUeba) { if (isAsync) { diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/scl/ScBaboonTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/scl/ScBaboonTranslator.scala index 15066b8c..20b3cda7 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/scl/ScBaboonTranslator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/scl/ScBaboonTranslator.scala @@ -202,7 +202,7 @@ class ScBaboonTranslator[F[+_, +_]: Error2]( } else { q"""$imports | - |${mappedTree}""".stripMargin + |$mappedTree""".stripMargin } val oPkgParts = o.pkg.parts.toList diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/scl/ScDefnTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/scl/ScDefnTranslator.scala index 1e6cbefd..9d76643b 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/scl/ScDefnTranslator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/scl/ScDefnTranslator.scala @@ -101,7 +101,7 @@ object ScDefnTranslator { } private def doTranslateFixtures(defn: DomainMember.User): F[NEList[BaboonIssue], List[Output]] = { - val srcRef = trans.toScTypeRefKeepForeigns(defn.id, domain, evo) + val srcRef = trans.toScTypeRefKeepForeigns(defn.id, domain, evo) val fixtureTreeOut = makeFixtureRepr(defn).map { fixtureTreeWithNs => Output( @@ -131,7 +131,7 @@ object ScDefnTranslator { } } private def doTranslateTest(defn: DomainMember.User): F[NEList[BaboonIssue], List[Output]] = { - val srcRef = trans.toScTypeRefKeepForeigns(defn.id, domain, evo) + val srcRef = trans.toScTypeRefKeepForeigns(defn.id, domain, evo) val codecTestOut = makeTestRepr(defn).map { codecTestWithNS => Output( @@ -359,10 +359,10 @@ object ScDefnTranslator { } val methods = service.methods.map { m => - val in = trans.asScRef(m.sig, domain, evo) - val out = m.out.map(trans.asScRef(_, domain, evo)) - val err = m.err.map(trans.asScRef(_, domain, evo)) - val servicePkgParts = name.pkg.parts.toList + val in = trans.asScRef(m.sig, domain, evo) + val out = m.out.map(trans.asScRef(_, domain, evo)) + val err = m.err.map(trans.asScRef(_, domain, evo)) + val servicePkgParts = name.pkg.parts.toList val scFqName: ScValue => String = { case t: ScValue.ScType if t.predef => t.name case t: ScValue.ScType => diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/scl/ScServiceWiringTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/scl/ScServiceWiringTranslator.scala index 89af67aa..e80d016e 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/scl/ScServiceWiringTranslator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/scl/ScServiceWiringTranslator.scala @@ -29,18 +29,22 @@ object ScServiceWiringTranslator { ServiceContextResolver.resolve(domain, "scala", target.language.serviceContext, target.language.pragmas) private def hasActiveJsonCodecs(service: Typedef.Service): Boolean = { - codecs.exists { c => - c.isInstanceOf[ScJsonCodecGenerator] && service.methods.forall { m => - c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) - } + codecs.exists { + c => + c.isInstanceOf[ScJsonCodecGenerator] && service.methods.forall { + m => + c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) + } } } private def hasActiveUebaCodecs(service: Typedef.Service): Boolean = { - codecs.exists { c => - c.isInstanceOf[ScUEBACodecGenerator] && service.methods.forall { m => - c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) - } + codecs.exists { + c => + c.isInstanceOf[ScUEBACodecGenerator] && service.methods.forall { + m => + c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) + } } } @@ -167,7 +171,7 @@ object ScServiceWiringTranslator { } private def renderFq(tree: TextTree[ScValue]): String = tree.mapRender { - case t: ScValue.ScType => if (t.predef) t.name else (t.pkg.parts :+ t.name).mkString(".") + case t: ScValue.ScType => if (t.predef) t.name else (t.pkg.parts :+ t.name).mkString(".") } // ========== noErrors mode ========== diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/swift/SwServiceWiringTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/swift/SwServiceWiringTranslator.scala index e8eb9660..728dbefc 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/swift/SwServiceWiringTranslator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/swift/SwServiceWiringTranslator.scala @@ -29,18 +29,22 @@ object SwServiceWiringTranslator { ServiceContextResolver.resolve(domain, "swift", target.language.serviceContext, target.language.pragmas) private def hasActiveJsonCodecs(service: Typedef.Service): Boolean = { - codecs.exists { c => - c.id == "Json" && service.methods.forall { m => - c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) - } + codecs.exists { + c => + c.id == "Json" && service.methods.forall { + m => + c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) + } } } private def hasActiveUebaCodecs(service: Typedef.Service): Boolean = { - codecs.exists { c => - c.id == "Ueba" && service.methods.forall { m => - c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) - } + codecs.exists { + c => + c.id == "Ueba" && service.methods.forall { + m => + c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) + } } } diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/typescript/TsBaboonTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/typescript/TsBaboonTranslator.scala index 75d49d8f..0eae68ce 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/typescript/TsBaboonTranslator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/typescript/TsBaboonTranslator.scala @@ -29,7 +29,7 @@ class TsBaboonTranslator[F[+_, +_]: Error2]( translated <- translateFamily(family) runtime <- sharedRuntime fixture <- sharedFixture - barrels = generateBarrels(translated ++ runtime) + barrels = generateBarrels(translated ++ runtime) rendered = ( translated ++ runtime ++ @@ -185,9 +185,9 @@ class TsBaboonTranslator[F[+_, +_]: Error2]( typesByModule.map { case (moduleId, types) => val typesString = types.map { - case t if aliasMap.contains(t) => s"${t.name} as ${aliasMap(t)}" - case TsType(_, name, Some(alias), _) => s"$name as $alias" - case t: TsValue.TsType => t.name + case t if aliasMap.contains(t) => s"${t.name} as ${aliasMap(t)}" + case TsType(_, name, Some(alias), _) => s"$name as $alias" + case t: TsValue.TsType => t.name }.mkString(", ") if (moduleId.path.startsWith(tsFileTools.definitionsBasePkg)) { definitionImport(moduleId, typesString) @@ -215,13 +215,14 @@ class TsBaboonTranslator[F[+_, +_]: Error2]( val conflicting = usedTypes.groupBy(_.name).filter(_._2.size > 1) conflicting.flatMap { case (name, group) => - val paths = group.map(t => t -> t.moduleId.path) - val commonLen = paths.map(_._2).reduce((a, b) => a.zip(b).takeWhile { case (x, y) => x == y }.map(_._1)).size + val paths = group.map(t => t -> t.moduleId.path) + val commonLen = paths.map(_._2).reduce((a, b) => a.zip(b).takeWhile { case (x, y) => x == y }.map(_._1)).size paths.map { case (t, path) => val distinguishing = path.drop(commonLen).dropRight(1) - val prefix = if (distinguishing.nonEmpty) distinguishing.mkString("_") - else path.dropRight(1).lastOption.getOrElse("m") + val prefix = + if (distinguishing.nonEmpty) distinguishing.mkString("_") + else path.dropRight(1).lastOption.getOrElse("m") t -> s"${prefix}_$name" } } @@ -229,21 +230,21 @@ class TsBaboonTranslator[F[+_, +_]: Error2]( /** Extract type names that a file defines (types whose module matches the file's own module). */ private def exportedNames(output: TsDefnTranslator.Output): Set[String] = { - output.tree.values - .collect { case t: TsValue.TsType if t.moduleId == output.module && !t.predef => t.name } - .toSet + output.tree.values.collect { case t: TsValue.TsType if t.moduleId == output.module && !t.predef => t.name }.toSet } private def generateBarrels(outputs: List[TsDefnTranslator.Output]): List[TsDefnTranslator.Output] = { val sfx = target.language.importSuffix - val definitionOutputs = outputs.filter(o => o.product == CompilerProduct.Definition || o.product == CompilerProduct.Runtime) + val definitionOutputs = outputs + .filter(o => o.product == CompilerProduct.Definition || o.product == CompilerProduct.Runtime) .filterNot(_.isBarrel) .filter(_.path.endsWith(".ts")) // Group files by their direct parent directory - val byDir = definitionOutputs.groupBy { o => - val idx = o.path.lastIndexOf('/') - if (idx >= 0) o.path.substring(0, idx) else "" + val byDir = definitionOutputs.groupBy { + o => + val idx = o.path.lastIndexOf('/') + if (idx >= 0) o.path.substring(0, idx) else "" } // Generate per-directory barrels with collision detection. @@ -259,9 +260,10 @@ class TsBaboonTranslator[F[+_, +_]: Error2]( val reexports = if (colliding.isEmpty) { // No collisions — simple export * for all files - sortedFiles.map { f => - val fname = f.path.drop(dir.length + 1).stripSuffix(".ts") - TextTree.text[TsValue](s"export * from './$fname$sfx';") + sortedFiles.map { + f => + val fname = f.path.drop(dir.length + 1).stripSuffix(".ts") + TextTree.text[TsValue](s"export * from './$fname$sfx';") } } else { // Has collisions — files with colliding names get individual qualified re-exports, @@ -273,13 +275,14 @@ class TsBaboonTranslator[F[+_, +_]: Error2]( if (myCollisions.isEmpty) { List(TextTree.text[TsValue](s"export * from './$fname$sfx';")) } else { - val safeNames = names -- colliding - val qualifier = fname.replace('.', '_').replace('-', '_').replace('/', '_') - val safeExport = if (safeNames.nonEmpty) { + val safeNames = names -- colliding + val qualifier = fname.replace('.', '_').replace('-', '_').replace('/', '_') + val safeExport = if (safeNames.nonEmpty) { List(TextTree.text[TsValue](s"export { ${safeNames.toList.sorted.mkString(", ")} } from './$fname$sfx';")) } else Nil - val qualifiedExports = myCollisions.toList.sorted.map { name => - TextTree.text[TsValue](s"export { $name as ${qualifier}_$name } from './$fname$sfx';") + val qualifiedExports = myCollisions.toList.sorted.map { + name => + TextTree.text[TsValue](s"export { $name as ${qualifier}_$name } from './$fname$sfx';") } safeExport ++ qualifiedExports } @@ -330,9 +333,9 @@ class TsBaboonTranslator[F[+_, +_]: Error2]( val basename = tsFileTools.basename(domain, lineage.evolution) convs.toList.map { conv => - val outputPath = s"$basename/${conv.fname}" - val moduleParts = tsFileTools.definitionsBasePkg ++ outputPath.stripSuffix(".ts").split('/').toList - val convModule = TsModuleId(moduleParts) + val outputPath = s"$basename/${conv.fname}" + val moduleParts = tsFileTools.definitionsBasePkg ++ outputPath.stripSuffix(".ts").split('/').toList + val convModule = TsModuleId(moduleParts) TsDefnTranslator.Output( outputPath, conv.conv, diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/typescript/TsCodecFixtureTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/typescript/TsCodecFixtureTranslator.scala index cb1962d5..0b8e8289 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/typescript/TsCodecFixtureTranslator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/typescript/TsCodecFixtureTranslator.scala @@ -143,16 +143,16 @@ object TsCodecFixtureTranslator { case TypeId.Builtins.tsu => translator.asTsType(TypeId.Builtins.tsu, domain, evo) match { case TsTypes.tsString => q"rnd.nextTsu().toISOString()" - case TsTypes.tsDate => q"rnd.nextTsu().date" - case _ => q"rnd.nextTsu()" + case TsTypes.tsDate => q"rnd.nextTsu().date" + case _ => q"rnd.nextTsu()" } case TypeId.Builtins.tso => translator.asTsType(TypeId.Builtins.tso, domain, evo) match { case TsTypes.tsString => q"rnd.nextTso().toISOString()" - case TsTypes.tsDate => q"rnd.nextTso().date" - case _ => q"rnd.nextTso()" + case TsTypes.tsDate => q"rnd.nextTso().date" + case _ => q"rnd.nextTso()" } - case TypeId.Builtins.bit => q"rnd.nextBit()" + case TypeId.Builtins.bit => q"rnd.nextBit()" case u: TypeId.User if enquiries.isEnum(tpe, domain) => val enumType = translator.asTsType(u, domain, evo, tsFileTools.definitionsBasePkg) diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/typescript/TsDefnTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/typescript/TsDefnTranslator.scala index 5738823d..d3e14850 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/typescript/TsDefnTranslator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/typescript/TsDefnTranslator.scala @@ -207,9 +207,10 @@ object TsDefnTranslator { val implementsClause = if (parents.nonEmpty) q"implements ${parents.map(tpe => q"$tpe").join(", ")}" else q"" - val toJsonFields = dto.fields.map { f => - val ref = s"this._${f.name.name}" - q"${f.name.name}: ${toJsonFieldExpr(f.tpe, ref)}" + val toJsonFields = dto.fields.map { + f => + val ref = s"this._${f.name.name}" + q"${f.name.name}: ${toJsonFieldExpr(f.tpe, ref)}" } val toJsonMethod = @@ -219,12 +220,14 @@ object TsDefnTranslator { | }; |}""".stripMargin - val withParamFields = dto.fields.map { f => - q"${f.name.name}?: ${typeTranslator.asTsRef(f.tpe, domain, evo, tsFileTools.definitionsBasePkg)}" + val withParamFields = dto.fields.map { + f => + q"${f.name.name}?: ${typeTranslator.asTsRef(f.tpe, domain, evo, tsFileTools.definitionsBasePkg)}" } - val withArgs = dto.fields.map { f => - q"'${f.name.name}' in overrides ? overrides.${f.name.name}! : this._${f.name.name}" + val withArgs = dto.fields.map { + f => + q"'${f.name.name}' in overrides ? overrides.${f.name.name}! : this._${f.name.name}" } val withMethod = @@ -234,12 +237,14 @@ object TsDefnTranslator { | ); |}""".stripMargin - val fromPlainParamFields = dto.fields.map { f => - q"${f.name.name}: ${typeTranslator.asTsRef(f.tpe, domain, evo, tsFileTools.definitionsBasePkg)}" + val fromPlainParamFields = dto.fields.map { + f => + q"${f.name.name}: ${typeTranslator.asTsRef(f.tpe, domain, evo, tsFileTools.definitionsBasePkg)}" } - val fromPlainArgs = dto.fields.map { f => - q"obj.${f.name.name}" + val fromPlainArgs = dto.fields.map { + f => + q"obj.${f.name.name}" } val fromPlainMethod = @@ -303,12 +308,13 @@ object TsDefnTranslator { } private def makeEnumRepr(enum: Typedef.Enum): DefnRepr = { - val enumName = enum.id.name.name - val branchesNames = enum.members.map(_.name) + val enumName = enum.id.name.name + val branchesNames = enum.members.map(_.name) val lowercaseValues = target.language.enumLowercaseValues - val branches = branchesNames.map { name => - val value = if (lowercaseValues) name.toLowerCase else name - q"$name = \"$value\"" + val branches = branchesNames.map { + name => + val value = if (lowercaseValues) name.toLowerCase else name + q"$name = \"$value\"" }.toSeq val parseComparison = if (lowercaseValues) "v === s.toLowerCase()" else "v === s" DefnRepr( @@ -343,13 +349,14 @@ object TsDefnTranslator { } } - val typeGuards = adt.members.toList.flatMap { mid => - domain.defs.meta.nodes(mid) match { - case DomainMember.User(_, _: Typedef.Dto, _, _) => - val branchName = mid.name.name - Some(q"export function is$branchName(value: $name): value is $branchName { return value instanceof $branchName; }") - case _ => None // skip contracts/interfaces — instanceof doesn't work on them - } + val typeGuards = adt.members.toList.flatMap { + mid => + domain.defs.meta.nodes(mid) match { + case DomainMember.User(_, _: Typedef.Dto, _, _) => + val branchName = mid.name.name + Some(q"export function is$branchName(value: $name): value is $branchName { return value instanceof $branchName; }") + case _ => None // skip contracts/interfaces — instanceof doesn't work on them + } } DefnRepr( diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/typescript/TsJsonCodecGenerator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/typescript/TsJsonCodecGenerator.scala index a737c098..d1e5125f 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/typescript/TsJsonCodecGenerator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/typescript/TsJsonCodecGenerator.scala @@ -210,7 +210,7 @@ class TsJsonCodecGenerator( case TypeId.Builtins.set => q"Array.from($ref).map(item => ${mkJsonEncoder(args.head, q"item")})" case TypeId.Builtins.map => - val keyType = args.head + val keyType = args.head val isRecord = trans.isStringKeyMap(tpe) keyType match { case TypeRef.Scalar(TypeId.Builtins.str) if isRecord => diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/typescript/TsServiceWiringTranslator.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/typescript/TsServiceWiringTranslator.scala index 9c725bcc..f502d21c 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/typescript/TsServiceWiringTranslator.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/translator/typescript/TsServiceWiringTranslator.scala @@ -35,18 +35,22 @@ object TsServiceWiringTranslator { ServiceContextResolver.resolve(domain, "typescript", target.language.serviceContext, target.language.pragmas) private def activeJsonCodec(service: Typedef.Service): Option[TsCodecTranslator] = { - codecs.find { c => - c.isInstanceOf[TsJsonCodecGenerator] && service.methods.forall { m => - c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) - } + codecs.find { + c => + c.isInstanceOf[TsJsonCodecGenerator] && service.methods.forall { + m => + c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) + } } } private def activeBinCodec(service: Typedef.Service): Option[TsCodecTranslator] = { - codecs.find { c => - c.isInstanceOf[TsUEBACodecGenerator] && service.methods.forall { m => - c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) - } + codecs.find { + c => + c.isInstanceOf[TsUEBACodecGenerator] && service.methods.forall { + m => + c.isActive(m.sig.id) && m.out.forall(o => c.isActive(o.id)) + } } } @@ -148,50 +152,53 @@ object TsServiceWiringTranslator { override def translateClient(defn: DomainMember.User): Option[TsDefnTranslator.Output] = { defn.defn match { case service: Typedef.Service => - val svcType = typeTranslator.asTsType(service.id, domain, evo, tsFileTools.definitionsBasePkg) + val svcType = typeTranslator.asTsType(service.id, domain, evo, tsFileTools.definitionsBasePkg) val jsonCodec = activeJsonCodec(service) val binCodec = activeBinCodec(service) - val clientMethods = service.methods.flatMap { m => - val inTypeRef = typeTranslator.asTsType(m.sig.id.asInstanceOf[TypeId.User], domain, evo, tsFileTools.definitionsBasePkg) - val inType = typeTranslator.asTsRef(m.sig, domain, evo, tsFileTools.definitionsBasePkg) - val outType = m.out.map(typeTranslator.asTsRef(_, domain, evo, tsFileTools.definitionsBasePkg)) - val retType = outType.getOrElse(q"void") - - val jsonMethod = jsonCodec.map { codec => - val inCodec = codec.codecName(inTypeRef) - val decodeOut = m.out match { - case Some(outRef) => - val outTypeRef = typeTranslator.asTsType(outRef.id.asInstanceOf[TypeId.User], domain, evo, tsFileTools.definitionsBasePkg) - val outCodec = codec.codecName(outTypeRef) - q"return $outCodec.instance.decode($tsBaboonCodecContext.Default, JSON.parse(resp)) as $retType;" - case None => q"return undefined as unknown as $retType;" + val clientMethods = service.methods.flatMap { + m => + val inTypeRef = typeTranslator.asTsType(m.sig.id.asInstanceOf[TypeId.User], domain, evo, tsFileTools.definitionsBasePkg) + val inType = typeTranslator.asTsRef(m.sig, domain, evo, tsFileTools.definitionsBasePkg) + val outType = m.out.map(typeTranslator.asTsRef(_, domain, evo, tsFileTools.definitionsBasePkg)) + val retType = outType.getOrElse(q"void") + + val jsonMethod = jsonCodec.map { + codec => + val inCodec = codec.codecName(inTypeRef) + val decodeOut = m.out match { + case Some(outRef) => + val outTypeRef = typeTranslator.asTsType(outRef.id.asInstanceOf[TypeId.User], domain, evo, tsFileTools.definitionsBasePkg) + val outCodec = codec.codecName(outTypeRef) + q"return $outCodec.instance.decode($tsBaboonCodecContext.Default, JSON.parse(resp)) as $retType;" + case None => q"return undefined as unknown as $retType;" + } + q"""public async ${m.name.name}Json(${ctxParamDecl}arg: $inType): Promise<$retType> { + | const encoded = JSON.stringify($inCodec.instance.encode($tsBaboonCodecContext.Default, arg)); + | const resp = await this.transportJson("${svcType.name}", "${m.name.name}", encoded); + | ${decodeOut.shift(4).trim} + |}""".stripMargin } - q"""public async ${m.name.name}Json(${ctxParamDecl}arg: $inType): Promise<$retType> { - | const encoded = JSON.stringify($inCodec.instance.encode($tsBaboonCodecContext.Default, arg)); - | const resp = await this.transportJson("${svcType.name}", "${m.name.name}", encoded); - | ${decodeOut.shift(4).trim} - |}""".stripMargin - } - val uebaMethod = binCodec.map { codec => - val inCodec = codec.codecName(inTypeRef) - val decodeOut = m.out match { - case Some(outRef) => - val outTypeRef = typeTranslator.asTsType(outRef.id.asInstanceOf[TypeId.User], domain, evo, tsFileTools.definitionsBasePkg) - val outCodec = codec.codecName(outTypeRef) - q"return $outCodec.instance.decode(ctx, new $tsBaboonBinReader(resp));" - case None => q"return undefined as unknown as $retType;" + val uebaMethod = binCodec.map { + codec => + val inCodec = codec.codecName(inTypeRef) + val decodeOut = m.out match { + case Some(outRef) => + val outTypeRef = typeTranslator.asTsType(outRef.id.asInstanceOf[TypeId.User], domain, evo, tsFileTools.definitionsBasePkg) + val outCodec = codec.codecName(outTypeRef) + q"return $outCodec.instance.decode(ctx, new $tsBaboonBinReader(resp));" + case None => q"return undefined as unknown as $retType;" + } + q"""public async ${m.name.name}(${ctxParamDecl}arg: $inType, ctx: $tsBaboonCodecContext = $tsBaboonCodecContext.Default): Promise<$retType> { + | const writer = new $tsBaboonBinWriter(); + | $inCodec.instance.encode(ctx, arg, writer); + | const resp = await this.transportUeba("${svcType.name}", "${m.name.name}", writer.toBytes()); + | ${decodeOut.shift(4).trim} + |}""".stripMargin } - q"""public async ${m.name.name}(${ctxParamDecl}arg: $inType, ctx: $tsBaboonCodecContext = $tsBaboonCodecContext.Default): Promise<$retType> { - | const writer = new $tsBaboonBinWriter(); - | $inCodec.instance.encode(ctx, arg, writer); - | const resp = await this.transportUeba("${svcType.name}", "${m.name.name}", writer.toBytes()); - | ${decodeOut.shift(4).trim} - |}""".stripMargin - } - uebaMethod.toList ++ jsonMethod.toList + uebaMethod.toList ++ jsonMethod.toList } if (clientMethods.isEmpty) return None @@ -251,27 +258,30 @@ object TsServiceWiringTranslator { val retTypeUeba = if (isAsync) "Promise" else "Uint8Array" val retTypeJson = if (isAsync) "Promise" else "string" - val implFields = services.map { s => - val svcType = typeTranslator.asTsType(s.id, domain, evo, tsFileTools.definitionsBasePkg) - q"${s.id.name.name}: $svcType" + val implFields = services.map { + s => + val svcType = typeTranslator.asTsType(s.id, domain, evo, tsFileTools.definitionsBasePkg) + q"${s.id.name.name}: $svcType" } val uebaFn = if (binCodecActive) { - val cases = services.flatMap { s => - activeBinCodec(s).map { _ => - val svcType = typeTranslator.asTsType(s.id, domain, evo, tsFileTools.definitionsBasePkg) - val wiringFnRef = TsValue.TsType( - TsValue.TsModuleId(tsFileTools.definitionsBasePkg ++ getWiringPathForService(s).stripSuffix(".ts").split('/').toList), - s"invokeUeba_${svcType.name}", - ) - if (resolved.noErrors) { - q"""case "${s.id.name.name}": - | return ${awaitPrefix}$wiringFnRef({ serviceName, methodName }, data, impls.${s.id.name.name}, ${ctxArgPass}ctx);""".stripMargin - } else { - q"""case "${s.id.name.name}": - | return ${awaitPrefix}$wiringFnRef({ serviceName, methodName }, data, impls.${s.id.name.name}, rt, ${ctxArgPass}ctx);""".stripMargin + val cases = services.flatMap { + s => + activeBinCodec(s).map { + _ => + val svcType = typeTranslator.asTsType(s.id, domain, evo, tsFileTools.definitionsBasePkg) + val wiringFnRef = TsValue.TsType( + TsValue.TsModuleId(tsFileTools.definitionsBasePkg ++ getWiringPathForService(s).stripSuffix(".ts").split('/').toList), + s"invokeUeba_${svcType.name}", + ) + if (resolved.noErrors) { + q"""case "${s.id.name.name}": + | return $awaitPrefix$wiringFnRef({ serviceName, methodName }, data, impls.${s.id.name.name}, ${ctxArgPass}ctx);""".stripMargin + } else { + q"""case "${s.id.name.name}": + | return $awaitPrefix$wiringFnRef({ serviceName, methodName }, data, impls.${s.id.name.name}, rt, ${ctxArgPass}ctx);""".stripMargin + } } - } } val rtParam = if (resolved.noErrors) "" else s"rt: $ibaboonServiceRt, " @@ -282,7 +292,7 @@ object TsServiceWiringTranslator { | methodName: string, | data: Uint8Array, | impls: {${implFields.join("; ")}}, - | ${rtParam}${ctxParamDecl}ctx: $tsBaboonCodecContext + | $rtParam${ctxParamDecl}ctx: $tsBaboonCodecContext |): $retTypeUeba { | switch (serviceName) { | ${cases.joinN().shift(8).trim} @@ -294,21 +304,23 @@ object TsServiceWiringTranslator { } else None val jsonFn = if (jsonCodecActive) { - val cases = services.flatMap { s => - activeJsonCodec(s).map { _ => - val svcType = typeTranslator.asTsType(s.id, domain, evo, tsFileTools.definitionsBasePkg) - val wiringFnRef = TsValue.TsType( - TsValue.TsModuleId(tsFileTools.definitionsBasePkg ++ getWiringPathForService(s).stripSuffix(".ts").split('/').toList), - s"invokeJson_${svcType.name}", - ) - if (resolved.noErrors) { - q"""case "${s.id.name.name}": - | return ${awaitPrefix}$wiringFnRef({ serviceName, methodName }, data, impls.${s.id.name.name}, ${ctxArgPass}ctx);""".stripMargin - } else { - q"""case "${s.id.name.name}": - | return ${awaitPrefix}$wiringFnRef({ serviceName, methodName }, data, impls.${s.id.name.name}, rt, ${ctxArgPass}ctx);""".stripMargin + val cases = services.flatMap { + s => + activeJsonCodec(s).map { + _ => + val svcType = typeTranslator.asTsType(s.id, domain, evo, tsFileTools.definitionsBasePkg) + val wiringFnRef = TsValue.TsType( + TsValue.TsModuleId(tsFileTools.definitionsBasePkg ++ getWiringPathForService(s).stripSuffix(".ts").split('/').toList), + s"invokeJson_${svcType.name}", + ) + if (resolved.noErrors) { + q"""case "${s.id.name.name}": + | return $awaitPrefix$wiringFnRef({ serviceName, methodName }, data, impls.${s.id.name.name}, ${ctxArgPass}ctx);""".stripMargin + } else { + q"""case "${s.id.name.name}": + | return $awaitPrefix$wiringFnRef({ serviceName, methodName }, data, impls.${s.id.name.name}, rt, ${ctxArgPass}ctx);""".stripMargin + } } - } } val rtParam = if (resolved.noErrors) "" else s"rt: $ibaboonServiceRt, " @@ -319,7 +331,7 @@ object TsServiceWiringTranslator { | methodName: string, | data: string, | impls: {${implFields.join("; ")}}, - | ${rtParam}${ctxParamDecl}ctx: $tsBaboonCodecContext + | $rtParam${ctxParamDecl}ctx: $tsBaboonCodecContext |): $retTypeJson { | switch (serviceName) { | ${cases.joinN().shift(8).trim} @@ -330,7 +342,7 @@ object TsServiceWiringTranslator { ) } else None - val tree = Seq(uebaFn, jsonFn).flatten.joinNN() + val tree = Seq(uebaFn, jsonFn).flatten.joinNN() val dispatcherPath = s"$fbase/baboon-dispatcher.ts" val dispatcherModule = TsValue.TsModuleId(tsFileTools.definitionsBasePkg ++ dispatcherPath.stripSuffix(".ts").split('/').toList) @@ -385,8 +397,8 @@ object TsServiceWiringTranslator { private val isAsync: Boolean = target.language.asyncServices - private val asyncPrefix: String = if (isAsync) "async " else "" - private val awaitPrefix: String = if (isAsync) "await " else "" + private val asyncPrefix: String = if (isAsync) "async " else "" + private val awaitPrefix: String = if (isAsync) "await " else "" // ========== noErrors mode ========== @@ -594,7 +606,10 @@ object TsServiceWiringTranslator { if (hasErrType) { q"""try { | const callResult = await impl.${m.name.name}(${ctxArgPass}decoded); - | return rt.leftMap(callResult, (err: unknown) => ({ tag: 'CallFailed' as const, method, domainError: err })) as unknown as ${renderContainer("BaboonWiringError", wireType)}; + | return rt.leftMap(callResult, (err: unknown) => ({ tag: 'CallFailed' as const, method, domainError: err })) as unknown as ${renderContainer( + "BaboonWiringError", + wireType, + )}; |} catch (ex: unknown) { | return rt.fail<$baboonWiringError, $wireType>({ tag: 'CallFailed', method, domainError: ex }); |}""".stripMargin @@ -686,13 +701,15 @@ object TsServiceWiringTranslator { svcType: TsValue.TsType, codec: TsCodecTranslator, ): TextTree[TsValue] = { - val cases = service.methods.map { m => - generateErrorsMethodBody( - m, codec, - decodeRef => q"$decodeRef.instance.decode($tsBaboonCodecContext.Default, JSON.parse(data))", - (encodeRef, _) => q"JSON.stringify($encodeRef.instance.encode($tsBaboonCodecContext.Default, v))", - "string", - ) + val cases = service.methods.map { + m => + generateErrorsMethodBody( + m, + codec, + decodeRef => q"$decodeRef.instance.decode($tsBaboonCodecContext.Default, JSON.parse(data))", + (encodeRef, _) => q"JSON.stringify($encodeRef.instance.encode($tsBaboonCodecContext.Default, v))", + "string", + ) }.join("\n") q"""export ${asyncPrefix}function invokeJson_${svcType.name}( @@ -715,13 +732,15 @@ object TsServiceWiringTranslator { svcType: TsValue.TsType, codec: TsCodecTranslator, ): TextTree[TsValue] = { - val cases = service.methods.map { m => - generateErrorsMethodBody( - m, codec, - decodeRef => q"(() => { const reader = new $tsBaboonBinReader(data); return $decodeRef.instance.decode(ctx, reader); })()", - (encodeRef, _) => q"(() => { const writer = new $tsBaboonBinWriter(); $encodeRef.instance.encode(ctx, v, writer); return writer.toBytes(); })()", - "Uint8Array", - ) + val cases = service.methods.map { + m => + generateErrorsMethodBody( + m, + codec, + decodeRef => q"(() => { const reader = new $tsBaboonBinReader(data); return $decodeRef.instance.decode(ctx, reader); })()", + (encodeRef, _) => q"(() => { const writer = new $tsBaboonBinWriter(); $encodeRef.instance.encode(ctx, v, writer); return writer.toBytes(); })()", + "Uint8Array", + ) }.join("\n") q"""export ${asyncPrefix}function invokeUeba_${svcType.name}( diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/typer/BaboonTyper.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/typer/BaboonTyper.scala index cf6f1974..b6019811 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/typer/BaboonTyper.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/typer/BaboonTyper.scala @@ -50,11 +50,13 @@ object BaboonTyper { } directRoots = rootExtractor.roots(indexedDefs) // Root aliases contribute their resolved targets to the root set - aliasRootIds: Set[TypeId] = typed.aliases.filter(_.root).flatMap { a => - enquiries.explode(a.resolvedTarget) - }.toSet + aliasRootIds: Set[TypeId] = typed.aliases + .filter(_.root).flatMap { + a => + enquiries.explode(a.resolvedTarget) + }.toSet aliasRoots = indexedDefs.filter { case (k, _) => aliasRootIds.contains(k) } - roots = directRoots ++ aliasRoots + roots = directRoots ++ aliasRoots predecessors <- buildDependencies( indexedDefs, roots, @@ -411,8 +413,9 @@ object BaboonTyper { (initial.map(m => (m.id, m)) ++ out.toSeq) .toUniqueMap(e => BaboonIssue.of(TyperIssue.NonUniqueTypedefs(e, meta))) } - aliases <- F.traverseAccumErrors(flattened.filter(_.defn.defn.isInstanceOf[RawAlias])) { scope => - translator(pkg, scope, out).resolveAliasInfo().map(_.get) + aliases <- F.traverseAccumErrors(flattened.filter(_.defn.defn.isInstanceOf[RawAlias])) { + scope => + translator(pkg, scope, out).resolveAliasInfo().map(_.get) } } yield { TyperOutput(indexed.values.toList, renames, aliases.toList) diff --git a/baboon-compiler/src/main/scala/io/septimalmind/baboon/typer/ScopeSupport.scala b/baboon-compiler/src/main/scala/io/septimalmind/baboon/typer/ScopeSupport.scala index 3acce74f..44734939 100644 --- a/baboon-compiler/src/main/scala/io/septimalmind/baboon/typer/ScopeSupport.scala +++ b/baboon-compiler/src/main/scala/io/septimalmind/baboon/typer/ScopeSupport.scala @@ -303,11 +303,12 @@ object ScopeSupport { scope: Scope[ExtendedRawDefn], ): Option[RawTypeRef] = { val needle = prefix.map(_.name).map(ScopeName.apply) ++: NEList(ScopeName(name.name)) - findScope(needle, scope).flatMap { found => - found.defn.defn match { - case alias: RawAlias => Some(alias.target) - case _ => None - } + findScope(needle, scope).flatMap { + found => + found.defn.defn match { + case alias: RawAlias => Some(alias.target) + case _ => None + } } } diff --git a/baboon-playground/src/compiler.ts b/baboon-playground/src/compiler.ts index fab6a5e7..0361fdff 100644 --- a/baboon-playground/src/compiler.ts +++ b/baboon-playground/src/compiler.ts @@ -131,6 +131,8 @@ const ALL_LANGUAGES = [ "java", "dart", "swift", + "graphql", + "openapi", ] as const; export type BaboonTargetLanguage = (typeof ALL_LANGUAGES)[number]; @@ -147,6 +149,8 @@ export const LANGUAGE_DISPLAY_NAMES: Record = { java: "Java", dart: "Dart", swift: "Swift", + graphql: "GraphQL", + openapi: "OpenAPI", }; export const LANGUAGE_TO_MONACO: Record = { @@ -159,6 +163,8 @@ export const LANGUAGE_TO_MONACO: Record = { java: "java", dart: "dart", swift: "swift", + graphql: "graphql", + openapi: "json", }; const EXTENSION_TO_LANGUAGE: Record = { @@ -171,9 +177,14 @@ const EXTENSION_TO_LANGUAGE: Record = { ".java": "java", ".dart": "dart", ".swift": "swift", + ".graphql": "graphql", }; -function detectLanguageByExtension(path: string): BaboonTargetLanguage | null { +function detectLanguageByPath(path: string): BaboonTargetLanguage | null { + // OpenAPI files are named openapi.json — detect by filename before extension + if (path.endsWith("/openapi.json") || path === "openapi.json") { + return "openapi"; + } for (const [ext, lang] of Object.entries(EXTENSION_TO_LANGUAGE)) { if (path.endsWith(ext)) { return lang; @@ -352,14 +363,19 @@ function remapErrorLocation( return { ...error, file: loc.file, line: loc.line }; } +// Schema-only backends that don't support codec/evolution options +const SCHEMA_ONLY_LANGUAGES: ReadonlySet = new Set(["graphql", "openapi"]); + function buildTargets(options: CompilerOptions): JSCompilerTarget[] { const generic: JSGenericOptions = { disableConversions: options.generic.disableConversions, }; return ALL_LANGUAGES.map((language) => { - const langOpts: JSLangOptions = options.languages[language]; const target: JSCompilerTarget = { language, generic }; - target[language] = langOpts; + if (!SCHEMA_ONLY_LANGUAGES.has(language)) { + const langOpts: JSLangOptions = options.languages[language]; + target[language] = langOpts; + } return target; }); } @@ -405,7 +421,7 @@ export async function compile( } for (const file of result.files ?? []) { - const lang = detectLanguageByExtension(file.path); + const lang = detectLanguageByPath(file.path); if (lang) { filesByLanguage.get(lang)!.push({ path: file.path, diff --git a/baboon-playground/src/options.ts b/baboon-playground/src/options.ts index c6952465..8791ca52 100644 --- a/baboon-playground/src/options.ts +++ b/baboon-playground/src/options.ts @@ -4,6 +4,10 @@ import { LANGUAGE_DISPLAY_NAMES, } from "./compiler.ts"; +// Schema-only backends don't have codec/evolution options +const SCHEMA_ONLY_LANGUAGES: ReadonlySet = new Set(["graphql", "openapi"]); +const CODEC_LANGUAGES = TARGET_LANGUAGES.filter((l) => !SCHEMA_ONLY_LANGUAGES.has(l)); + export interface GenericOptions { disableConversions: boolean; } @@ -143,7 +147,7 @@ export class OptionsPanel { this.scrollBody.innerHTML = ""; this.scrollBody.appendChild(this.renderGenericSection()); this.scrollBody.appendChild(this.renderGlobalLanguageSection()); - for (const lang of TARGET_LANGUAGES) { + for (const lang of CODEC_LANGUAGES) { this.scrollBody.appendChild(this.renderLanguageSection(lang)); } } @@ -190,7 +194,7 @@ export class OptionsPanel { private computeGlobalState(key: keyof LanguageOptions): boolean | null { let allTrue = true; let allFalse = true; - for (const lang of TARGET_LANGUAGES) { + for (const lang of CODEC_LANGUAGES) { if (this.options.languages[lang][key]) { allFalse = false; } else { @@ -215,7 +219,7 @@ export class OptionsPanel { def.description, state === true, (checked) => { - for (const lang of TARGET_LANGUAGES) { + for (const lang of CODEC_LANGUAGES) { this.options.languages[lang][def.key] = checked; } this.onChange(this.options); diff --git a/test/gql-stub/package-lock.json b/test/gql-stub/package-lock.json new file mode 100644 index 00000000..5c0a636f --- /dev/null +++ b/test/gql-stub/package-lock.json @@ -0,0 +1,24 @@ +{ + "name": "baboon-graphql-test", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "baboon-graphql-test", + "version": "1.0.0", + "dependencies": { + "graphql": "^16.11.0" + } + }, + "node_modules/graphql": { + "version": "16.13.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz", + "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + } + } +} diff --git a/test/gql-stub/package.json b/test/gql-stub/package.json new file mode 100644 index 00000000..6d08bae1 --- /dev/null +++ b/test/gql-stub/package.json @@ -0,0 +1,11 @@ +{ + "name": "baboon-graphql-test", + "version": "1.0.0", + "private": true, + "scripts": { + "test": "node validate.mjs" + }, + "dependencies": { + "graphql": "^16.11.0" + } +} diff --git a/test/gql-stub/validate.mjs b/test/gql-stub/validate.mjs new file mode 100644 index 00000000..1e8a8e23 --- /dev/null +++ b/test/gql-stub/validate.mjs @@ -0,0 +1,60 @@ +import { readFileSync, readdirSync, statSync } from 'fs'; +import { join } from 'path'; +import { buildSchema, parse } from 'graphql'; + +function findGraphqlFiles(dir) { + const results = []; + for (const entry of readdirSync(dir)) { + const full = join(dir, entry); + if (statSync(full).isDirectory()) { + results.push(...findGraphqlFiles(full)); + } else if (entry.endsWith('.graphql')) { + results.push(full); + } + } + return results; +} + +const schemasDir = process.argv[2]; +if (!schemasDir) { + console.error('Usage: node validate.mjs '); + process.exit(1); +} + +const files = findGraphqlFiles(schemasDir); +if (files.length === 0) { + console.error(`No .graphql files found in ${schemasDir}`); + process.exit(1); +} + +let errors = 0; + +for (const file of files.sort()) { + const content = readFileSync(file, 'utf-8'); + + // Step 1: Parse the SDL (checks syntax) + try { + parse(content); + } catch (e) { + console.error(`PARSE ERROR in ${file}: ${e.message}`); + errors++; + continue; + } + + // Step 2: Build schema (checks type references, consistency) + // We add a dummy Query type since buildSchema requires it, + // but only if the schema doesn't already define one. + const schemaWithQuery = content.includes('type Query') ? content : content + '\ntype Query { _unused: Boolean }\n'; + try { + buildSchema(schemaWithQuery); + console.log(`OK: ${file}`); + } catch (e) { + console.error(`SCHEMA ERROR in ${file}: ${e.message}`); + errors++; + } +} + +console.log(`\nValidated ${files.length} files, ${errors} errors.`); +if (errors > 0) { + process.exit(1); +} diff --git a/test/oas-stub/package-lock.json b/test/oas-stub/package-lock.json new file mode 100644 index 00000000..6fca5a99 --- /dev/null +++ b/test/oas-stub/package-lock.json @@ -0,0 +1,175 @@ +{ + "name": "baboon-openapi-test", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "baboon-openapi-test", + "version": "1.0.0", + "dependencies": { + "@apidevtools/swagger-parser": "^10.1.1" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.7.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz", + "integrity": "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.1.1.tgz", + "integrity": "sha512-u/kozRnsPO/x8QtKYJOqoGtC4kH6yg1lfYkB9Au0WhYB0FNLpyFusttQtvhlwjtG3rOwiRz4D8DnnXa8iEpIKA==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "11.7.2", + "@apidevtools/openapi-schemas": "^2.1.0", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "call-me-maybe": "^1.0.2" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + } + } +} diff --git a/test/oas-stub/package.json b/test/oas-stub/package.json new file mode 100644 index 00000000..b46ab6e5 --- /dev/null +++ b/test/oas-stub/package.json @@ -0,0 +1,11 @@ +{ + "name": "baboon-openapi-test", + "version": "1.0.0", + "private": true, + "scripts": { + "test": "node validate.mjs" + }, + "dependencies": { + "@apidevtools/swagger-parser": "^10.1.1" + } +} diff --git a/test/oas-stub/validate.mjs b/test/oas-stub/validate.mjs new file mode 100644 index 00000000..6baf716b --- /dev/null +++ b/test/oas-stub/validate.mjs @@ -0,0 +1,58 @@ +import { readFileSync, readdirSync, statSync } from 'fs'; +import { join } from 'path'; +import SwaggerParser from '@apidevtools/swagger-parser'; + +function findJsonFiles(dir) { + const results = []; + for (const entry of readdirSync(dir)) { + const full = join(dir, entry); + if (statSync(full).isDirectory()) { + results.push(...findJsonFiles(full)); + } else if (entry.endsWith('.json')) { + results.push(full); + } + } + return results; +} + +const schemasDir = process.argv[2]; +if (!schemasDir) { + console.error('Usage: node validate.mjs '); + process.exit(1); +} + +const files = findJsonFiles(schemasDir); +if (files.length === 0) { + console.error(`No .json files found in ${schemasDir}`); + process.exit(1); +} + +let errors = 0; + +for (const file of files.sort()) { + const content = readFileSync(file, 'utf-8'); + + // Step 1: Parse JSON (checks syntax) + let doc; + try { + doc = JSON.parse(content); + } catch (e) { + console.error(`JSON PARSE ERROR in ${file}: ${e.message}`); + errors++; + continue; + } + + // Step 2: Validate as OpenAPI using swagger-parser + try { + await SwaggerParser.validate(structuredClone(doc)); + console.log(`OK: ${file}`); + } catch (e) { + console.error(`OPENAPI VALIDATION ERROR in ${file}: ${e.message}`); + errors++; + } +} + +console.log(`\nValidated ${files.length} files, ${errors} errors.`); +if (errors > 0) { + process.exit(1); +} diff --git a/test/services/sc/src/main/scala/example/PetStoreClient.scala b/test/services/sc/src/main/scala/example/PetStoreClient.scala index a443f257..456af583 100644 --- a/test/services/sc/src/main/scala/example/PetStoreClient.scala +++ b/test/services/sc/src/main/scala/example/PetStoreClient.scala @@ -8,7 +8,8 @@ import baboon.runtime.shared.BaboonCodecContext object PetStoreClient { private val ctx: BaboonCodecContext = BaboonCodecContext.Default - private val httpClient: HttpClient = HttpClient.newBuilder() + private val httpClient: HttpClient = HttpClient + .newBuilder() .version(HttpClient.Version.HTTP_1_1) .build() @@ -19,54 +20,54 @@ object PetStoreClient { // Add Buddy val addBuddyIn = petstore.api.petstore.addpet.In( - name = "Buddy", + name = "Buddy", status = petstore.api.PetStatus.Available, - tag = Some("dog"), + tag = Some("dog"), ) val addBuddyJson = petstore.api.petstore.addpet.In_JsonCodec.instance.encode(ctx, addBuddyIn).noSpaces val addBuddyResp = post(base, "/PetStore/addPet", addBuddyJson) - val addBuddyOut = decode(petstore.api.petstore.addpet.Out_JsonCodec.instance, addBuddyResp) - val buddyId = addBuddyOut.pet.id + val addBuddyOut = decode(petstore.api.petstore.addpet.Out_JsonCodec.instance, addBuddyResp) + val buddyId = addBuddyOut.pet.id assert(addBuddyOut.pet.name == "Buddy", s"expected name Buddy, got ${addBuddyOut.pet.name}") // Add Whiskers val addWhiskersIn = petstore.api.petstore.addpet.In( - name = "Whiskers", + name = "Whiskers", status = petstore.api.PetStatus.Pending, - tag = Some("cat"), + tag = Some("cat"), ) val addWhiskersJson = petstore.api.petstore.addpet.In_JsonCodec.instance.encode(ctx, addWhiskersIn).noSpaces val addWhiskersResp = post(base, "/PetStore/addPet", addWhiskersJson) - val addWhiskersOut = decode(petstore.api.petstore.addpet.Out_JsonCodec.instance, addWhiskersResp) - val whiskersId = addWhiskersOut.pet.id + val addWhiskersOut = decode(petstore.api.petstore.addpet.Out_JsonCodec.instance, addWhiskersResp) + val whiskersId = addWhiskersOut.pet.id assert(addWhiskersOut.pet.name == "Whiskers", s"expected name Whiskers, got ${addWhiskersOut.pet.name}") // List pets (expect 2) - val listIn = petstore.api.petstore.listpets.In() + val listIn = petstore.api.petstore.listpets.In() val listJson = petstore.api.petstore.listpets.In_JsonCodec.instance.encode(ctx, listIn).noSpaces val listResp = post(base, "/PetStore/listPets", listJson) - val listOut = decode(petstore.api.petstore.listpets.Out_JsonCodec.instance, listResp) + val listOut = decode(petstore.api.petstore.listpets.Out_JsonCodec.instance, listResp) assert(listOut.pets.size == 2, s"expected 2 pets, got ${listOut.pets.size}") // Get Buddy - val getBuddyIn = petstore.api.petstore.getpet.In(id = buddyId) + val getBuddyIn = petstore.api.petstore.getpet.In(id = buddyId) val getBuddyJson = petstore.api.petstore.getpet.In_JsonCodec.instance.encode(ctx, getBuddyIn).noSpaces val getBuddyResp = post(base, "/PetStore/getPet", getBuddyJson) - val getBuddyOut = decode(petstore.api.petstore.getpet.Out_JsonCodec.instance, getBuddyResp) + val getBuddyOut = decode(petstore.api.petstore.getpet.Out_JsonCodec.instance, getBuddyResp) assert(getBuddyOut.pet.name == "Buddy", s"expected Buddy, got ${getBuddyOut.pet.name}") assert(getBuddyOut.pet.status == petstore.api.PetStatus.Available, s"expected Available, got ${getBuddyOut.pet.status}") assert(getBuddyOut.pet.tag.contains("dog"), s"expected tag dog, got ${getBuddyOut.pet.tag}") // Delete Whiskers - val deleteIn = petstore.api.petstore.deletepet.In(id = whiskersId) + val deleteIn = petstore.api.petstore.deletepet.In(id = whiskersId) val deleteJson = petstore.api.petstore.deletepet.In_JsonCodec.instance.encode(ctx, deleteIn).noSpaces val deleteResp = post(base, "/PetStore/deletePet", deleteJson) - val deleteOut = decode(petstore.api.petstore.deletepet.Out_JsonCodec.instance, deleteResp) + val deleteOut = decode(petstore.api.petstore.deletepet.Out_JsonCodec.instance, deleteResp) assert(deleteOut.deleted, s"expected deleted=true, got ${deleteOut.deleted}") // List pets again (expect 1) val list2Resp = post(base, "/PetStore/listPets", listJson) - val list2Out = decode(petstore.api.petstore.listpets.Out_JsonCodec.instance, list2Resp) + val list2Out = decode(petstore.api.petstore.listpets.Out_JsonCodec.instance, list2Resp) assert(list2Out.pets.size == 1, s"expected 1 pet, got ${list2Out.pets.size}") assert(list2Out.pets.head.name == "Buddy", s"expected remaining pet Buddy, got ${list2Out.pets.head.name}") @@ -74,7 +75,8 @@ object PetStoreClient { } private def post(base: String, path: String, body: String): String = { - val request = HttpRequest.newBuilder() + val request = HttpRequest + .newBuilder() .uri(URI.create(s"$base$path")) .expectContinue(false) .timeout(Duration.ofSeconds(10)) diff --git a/test/services/sc/src/main/scala/example/PetStoreImpl.scala b/test/services/sc/src/main/scala/example/PetStoreImpl.scala index d95ff0b5..a7a81627 100644 --- a/test/services/sc/src/main/scala/example/PetStoreImpl.scala +++ b/test/services/sc/src/main/scala/example/PetStoreImpl.scala @@ -5,7 +5,7 @@ import scala.collection.mutable class PetStoreImpl extends petstore.api.PetStore { private val idCounter = new AtomicLong(0) - private val pets = mutable.Map.empty[Long, petstore.api.Pet] + private val pets = mutable.Map.empty[Long, petstore.api.Pet] def reset(): Unit = { idCounter.set(0) @@ -15,10 +15,10 @@ class PetStoreImpl extends petstore.api.PetStore { override def addPet(arg: petstore.api.petstore.addpet.In): petstore.api.petstore.addpet.Out = { val id = idCounter.incrementAndGet() val pet = petstore.api.Pet( - id = id, - name = arg.name, + id = id, + name = arg.name, status = arg.status, - tag = arg.tag, + tag = arg.tag, ) pets.put(id, pet) petstore.api.petstore.addpet.Out(pet) diff --git a/test/services/sc/src/main/scala/example/PetStoreServer.scala b/test/services/sc/src/main/scala/example/PetStoreServer.scala index ab9117ce..fadbb53e 100644 --- a/test/services/sc/src/main/scala/example/PetStoreServer.scala +++ b/test/services/sc/src/main/scala/example/PetStoreServer.scala @@ -7,55 +7,69 @@ import baboon.runtime.shared.{BaboonCodecContext, BaboonMethodId} import petstore.api.PetStoreWiring object PetStoreServer { - private val impl = new PetStoreImpl + private val impl = new PetStoreImpl private val ctx: BaboonCodecContext = BaboonCodecContext.Default def start(host: String, port: Int): Unit = { val server = HttpServer.create(new InetSocketAddress(host, port), 0) - server.createContext("/health", (exchange: HttpExchange) => { - sendResponse(exchange, 200, "ok") - }) - - server.createContext("/reset", (exchange: HttpExchange) => { - impl.reset() - sendResponse(exchange, 200, "ok") - }) + server.createContext( + "/health", + (exchange: HttpExchange) => { + sendResponse(exchange, 200, "ok") + }, + ) - server.createContext("/shutdown", (exchange: HttpExchange) => { - if (exchange.getRequestMethod != "POST") { - sendResponse(exchange, 405, "Method Not Allowed") - } else { + server.createContext( + "/reset", + (exchange: HttpExchange) => { + impl.reset() sendResponse(exchange, 200, "ok") - val stopper = new Thread(() => { - server.stop(0) - System.exit(0) - }) - stopper.setDaemon(false) - stopper.start() - } - }) + }, + ) + + server.createContext( + "/shutdown", + (exchange: HttpExchange) => { + if (exchange.getRequestMethod != "POST") { + sendResponse(exchange, 405, "Method Not Allowed") + } else { + sendResponse(exchange, 200, "ok") + val stopper = new Thread( + () => { + server.stop(0) + System.exit(0) + } + ) + stopper.setDaemon(false) + stopper.start() + } + }, + ) - server.createContext("/", (exchange: HttpExchange) => { - val path = exchange.getRequestURI.getPath - val parts = path.split("/").filter(_.nonEmpty) - if (parts.length == 2) { - val service = parts(0) - val method = parts(1) - val body = new String(exchange.getRequestBody.readAllBytes(), "UTF-8") - try { - val methodId = BaboonMethodId(service, method) - val result = PetStoreWiring.invokeJson(methodId, body, impl, ctx) - exchange.getResponseHeaders.set("Content-Type", "application/json") - sendResponse(exchange, 200, result) - } catch { - case e: Exception => - sendResponse(exchange, 500, e.getMessage) + server.createContext( + "/", + (exchange: HttpExchange) => { + val path = exchange.getRequestURI.getPath + val parts = path.split("/").filter(_.nonEmpty) + if (parts.length == 2) { + val service = parts(0) + val method = parts(1) + val body = new String(exchange.getRequestBody.readAllBytes(), "UTF-8") + try { + val methodId = BaboonMethodId(service, method) + val result = PetStoreWiring.invokeJson(methodId, body, impl, ctx) + exchange.getResponseHeaders.set("Content-Type", "application/json") + sendResponse(exchange, 200, result) + } catch { + case e: Exception => + sendResponse(exchange, 500, e.getMessage) + } + } else { + sendResponse(exchange, 404, "Not Found") } - } else { - sendResponse(exchange, 404, "Not Found") - } - }) + }, + ) server.setExecutor(null) server.start()