Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0306801
This seems to work up to problems with the ordering of fields
Quafadas Apr 14, 2023
879c82c
Now with tests
Quafadas Apr 14, 2023
09b1620
That should test upickle
Quafadas Apr 14, 2023
7cc892f
See if this runs the upickle suite
Quafadas Apr 14, 2023
3027c8d
Remove extraneous declarations
Quafadas Apr 14, 2023
ecb282f
Greatly improve enum encoding
Quafadas Apr 21, 2023
5c23948
Fix linting error
Quafadas Apr 21, 2023
c8afd56
Oops, remove non existing language
Quafadas Apr 21, 2023
017c758
Cant have wildcard enums
Quafadas Apr 21, 2023
fc97a63
Merge branch 'master' into improved-scala3-enums
dvdsgl May 16, 2023
ff38452
Merge branch 'quicktype:master' into improved-scala3-enums
Quafadas May 17, 2023
77eeee2
Try to fix some of the linting problems
Quafadas May 17, 2023
cfca165
Undo bad linting suggestions
Quafadas May 17, 2023
721b70a
Merge branch 'master' into improved-scala3-enums
dvdsgl Jun 3, 2023
c5e70b8
Merge branch 'master' into improved-scala3-enums
dvdsgl Jun 4, 2023
a332dbb
Merge branch 'master' into improved-scala3-enums
dvdsgl Feb 14, 2024
54f3e9b
Merge branch 'master' into improved-scala3-enums
dvdsgl Feb 14, 2024
b972d13
Merge master and resolve conflicts
schani Jul 9, 2026
30bf5a2
Merge origin/master into PR #2246 head
schani Jul 19, 2026
6022de7
scala3: real enum codecs, safer union decoding, top-level primitives
schani Jul 20, 2026
ba661fe
scala3: fix the upickle renderer's unions, enums, and null members
schani Jul 20, 2026
1f6a642
Register the scala3-upickle fixture and overhaul the scala3 skip lists
schani Jul 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ jobs:
- ruby
- php,schema-php
- scala3,schema-scala3
- scala3-upickle,schema-scala3-upickle
- elixir,schema-elixir,graphql-elixir
- comment-injection-treesitter,comment-injection-typescript,comment-injection-typescript-zod,comment-injection-typescript-effect-schema

Expand Down
114 changes: 76 additions & 38 deletions packages/quicktype-core/src/language/Scala3/CirceRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import type {
UnionType,
} from "../../Type/index.js";

import { stringEscape } from "../../support/Strings.js";

import { Scala3Renderer } from "./Scala3Renderer.js";
import { wrapOption } from "./utils.js";
import { unionMemberSortOrder, wrapOption } from "./utils.js";

export class CirceRenderer extends Scala3Renderer {
private readonly seenUnionTypes: string[] = [];
Expand Down Expand Up @@ -55,7 +57,13 @@ export class CirceRenderer extends Scala3Renderer {
paramName,
")",
],
(_) => ["Encoder.encodeString(", paramName, ")"],
(enumType) => [
"summon[Encoder[",
this.scalaType(enumType),
"]].apply(",
paramName,
")",
],
(unionType) => {
const nullable = nullableFromUnion(unionType);
if (nullable !== null) {
Expand Down Expand Up @@ -103,44 +111,70 @@ export class CirceRenderer extends Scala3Renderer {

protected emitEnumDefinition(e: EnumType, enumName: Name): void {
this.emitDescription(this.descriptionForType(e));
this.ensureBlankLine();

// Enum cases are styled Scala identifiers; the codecs below map
// them back to the original JSON strings, which can be anything
// (keywords, `"_"`, `""`, …).
this.emitLine(["enum ", enumName, " : "]);
this.indent(() => {
this.forEachEnumCase(e, "none", (name) => {
this.emitLine("case ", name);
});
});
this.ensureBlankLine();
this.emitItem(["type ", enumName, " = "]);
let count = e.cases.size;
this.forEachEnumCase(e, "none", (_, jsonName) => {
// if (!(jsonName == "")) {
/* const backticks =
shouldAddBacktick(jsonName) ||
jsonName.includes(" ") ||
!isNaN(parseInt(jsonName.charAt(0)))
if (backticks) {this.emitItem("`")} else */
this.emitItem(['"', jsonName, '"']);
// if (backticks) {this.emitItem("`")}
if (--count > 0) this.emitItem([" | "]);
// } else {
// --count
// }

this.emitLine([
"given Decoder[",
enumName,
"] = Decoder.decodeString.emap {",
]);
this.indent(() => {
// `scala.` in case a generated type is named Right/Left.
this.forEachEnumCase(e, "none", (name, jsonName) => {
this.emitLine([
`case "${stringEscape(jsonName)}" => scala.Right(`,
enumName,
".",
name,
")",
]);
});
this.emitLine([
'case other => scala.Left("invalid ',
enumName,
': " + other)',
]);
});
this.emitLine("}");
this.emitLine([
"given Encoder[",
enumName,
"] = Encoder.encodeString.contramap {",
]);
this.indent(() => {
this.forEachEnumCase(e, "none", (name, jsonName) => {
this.emitLine([
"case ",
enumName,
".",
name,
` => "${stringEscape(jsonName)}"`,
]);
});
});
this.emitLine("}");
this.ensureBlankLine();
}

protected emitHeader(): void {
super.emitHeader();

this.emitLine("import scala.util.Try");
this.emitLine("import io.circe.syntax._");
this.emitLine("import io.circe._");
this.emitLine("import cats.syntax.functor._");
this.ensureBlankLine();

this.emitLine("// For serialising string unions");
this.emitLine(
"given [A <: Singleton](using A <:< String): Decoder[A] = Decoder.decodeString.emapTry(x => Try(x.asInstanceOf[A])) ",
);
this.emitLine(
"given [A <: Singleton](using ev: A <:< String): Encoder[A] = Encoder.encodeString.contramap(ev) ",
);
this.ensureBlankLine();
this.emitLine(
"// If a union has a null in, then we'll need this too... ",
);
Expand All @@ -153,9 +187,9 @@ export class CirceRenderer extends Scala3Renderer {
this.emitLine([
"given (using ev : ",
elementType,
"): Encoder[Map[String,",
"): Encoder[Seq[",
elementType,
"]] = Encoder.encodeMap[String, ",
"]] = Encoder.encodeSeq[",
elementType,
"]",
]);
Expand All @@ -177,15 +211,9 @@ export class CirceRenderer extends Scala3Renderer {
}

protected emitUnionDefinition(u: UnionType, unionName: Name): void {
function sortBy(t: Type): string {
const kind = t.kind;
if (kind === "class") return kind;
return `_${kind}`;
}

this.emitDescription(this.descriptionForType(u));

const [maybeNull, nonNulls] = removeNullFromUnion(u, sortBy);
const [maybeNull, nonNulls] = removeNullFromUnion(u, false);
const theTypes: Sourcelike[] = [];
this.forEachUnionMember(u, nonNulls, "none", null, (_, t) => {
theTypes.push(this.scalaType(t));
Expand All @@ -205,10 +233,20 @@ export class CirceRenderer extends Scala3Renderer {
this.ensureBlankLine();
if (!this.seenUnionTypes.some((y) => y === thisUnionType)) {
this.seenUnionTypes.push(thisUnionType);
// The decoders are tried in order, most discriminating
// first (see `unionMemberSortOrder`): circe's numeric
// decoders accept strings like "5", so the string decoder
// has to get a shot before them.
const sourceLikeTypes: Array<[Sourcelike, Type]> = [];
this.forEachUnionMember(u, nonNulls, "none", null, (_, t) => {
sourceLikeTypes.push([this.scalaType(t), t]);
});
this.forEachUnionMember(
u,
nonNulls,
"none",
unionMemberSortOrder,
(_, t) => {
sourceLikeTypes.push([this.scalaType(t), t]);
},
);
if (maybeNull !== null) {
sourceLikeTypes.push([
this.nameForUnionMember(u, maybeNull),
Expand Down
46 changes: 10 additions & 36 deletions packages/quicktype-core/src/language/Scala3/Scala3Renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ import {
import { keywords } from "./constants.js";
import type { scala3Options } from "./language.js";
import {
backtickedName,
enumCaseNameStyle,
lowerNamingFunction,
scalaNameStyle,
shouldAddBacktick,
upperNamingFunction,
wrapOption,
} from "./utils.js";
Expand Down Expand Up @@ -61,7 +62,7 @@ export class Scala3Renderer extends ConvenienceRenderer {
_: EnumType,
_enumName: Name,
): ForbiddenWordsInfo {
return { names: [], includeGlobalForbidden: true };
return { names: ["_"], includeGlobalForbidden: true };
}

protected forbiddenForUnionMembers(
Expand Down Expand Up @@ -91,7 +92,7 @@ export class Scala3Renderer extends ConvenienceRenderer {
}

protected makeEnumCaseNamer(): Namer {
return funPrefixNamer("upper", (s) => s.replace(" ", "")); // TODO - add backticks where appropriate
return funPrefixNamer("upper", enumCaseNameStyle);
}

protected emitDescriptionBlock(lines: Sourcelike[]): void {
Expand Down Expand Up @@ -262,14 +263,9 @@ export class Scala3Renderer extends ConvenienceRenderer {
emit();
}

const nameNeedsBackticks =
jsonName.endsWith("_") || shouldAddBacktick(jsonName);
const nameWithBackticks = nameNeedsBackticks
? `\`${jsonName}\``
: jsonName;
this.emitLine(
"val ",
nameWithBackticks,
backtickedName(jsonName),
" : ",
scalaType(p),
p.isOptional
Expand Down Expand Up @@ -301,32 +297,8 @@ export class Scala3Renderer extends ConvenienceRenderer {
this.emitBlock(
["enum ", enumName, " : "],
() => {
let count = e.cases.size;
if (count > 0) {
this.emitItem("\t case ");
}

this.forEachEnumCase(e, "none", (name, jsonName) => {
if (!(jsonName === "")) {
const backticks =
shouldAddBacktick(jsonName) ||
jsonName.includes(" ") ||
!Number.isNaN(
Number.parseInt(jsonName.charAt(0), 10),
);
if (backticks) {
this.emitItem("`");
}

this.emitItemOnce([name]);
if (backticks) {
this.emitItem("`");
}

if (--count > 0) this.emitItem([","]);
} else {
--count;
}
this.forEachEnumCase(e, "none", (name) => {
this.emitLine("case ", name);
});
},
"none",
Expand Down Expand Up @@ -361,12 +333,14 @@ export class Scala3Renderer extends ConvenienceRenderer {
protected emitSourceStructure(): void {
this.emitHeader();

// Top-level arrays, maps
// Top-level arrays, maps, and primitives
this.forEachTopLevel("leading", (t, name) => {
if (t instanceof ArrayType) {
this.emitTopLevelArray(t, name);
} else if (t instanceof MapType) {
this.emitTopLevelMap(t, name);
} else if (t.isPrimitive()) {
this.emitLine(["type ", name, " = ", this.scalaType(t)]);
}
});

Expand Down
Loading
Loading