lexgen csharp: codegen defects found generating exchange.recipe.* (real-world third-party lexicon) #45
Labels
No labels
breaking-change
bug
documentation
duplicate
enhancement
good first issue
help wanted
performance
question
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Grandiras/ATProto.NET#45
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Dogfooding
atproto-lexgen csharpagainst the published recipe.exchange lexicons (exchange.recipe.recipe/.collection/.profile/.defs) for the Mise appview surfaced these defects in the generated C#:Recipe.g.csends with a stray extra}(file does not compile).imageblob property inside theimageobject def generatespublic required BlobRef Imageinsideclass Image(member named same as enclosing type; compile error). Needs a rename strategy (e.g.BloborImageBlob).BlobRefunqualified — nousing ATProtoNet;/qualification forBlobRef, so the file doesn't resolve it.JsonElementfallbacks — optionalunionfields and inlineobjectschemas (e.g.attribution,nutrition) generatepublic JsonElement X { get; init; }. Serializingdefault(JsonElement)(ValueKind.Undefined) throws — should at minimum beJsonElement?, ideally a generated union base +[JsonPolymorphic]variants and a nested class for inline objects.app.bsky.embed.defs#aspectRatiogeneratesMise.Core.Lexicon.App.Bsky.Embed.AspectRatio(nonexistent) instead of mapping to the SDK's existingapp.bskymodel types.AtProtoRecord— generated record classes re-declare$type/createdAtinstead of subclassing the SDK base, so they miss the SDK's record ergonomics.knownValuessets like cookingMethod*) generate ~100 separate static classes; grouping per family (one static class with const members) would be far more usable.Repro:
atproto-lexgen csharp --input <the four exchange.recipe jsons> --output out --namespace Mise.Core.Lexicon. Mise is hand-maintaining patched copies for now (see Mise repo, src/Mise.Core/Lexicon/Exchange/Recipe/) — that folder is the reference for what the generator ideally emits.🤖 On it — follow along in the action run; the reply will link the full session transcript.
Pushed branch
claude/issue-45(compare).I fetched the real schemas rather than approximating them — resolved
_lexicon.recipe.exchange→did:plc:4cx7ts7lqgjtsfquo53qo3sz→ its PDS, and pulled the fourcom.atproto.lexicon.schemarecords — then fixed the generator against them.All seven items
}after a file-scopednamespace X;. Removed.AtProtoRecord'sType/CreatedAtare renamed (imageblob in defimage→ImageBlob; also…Ref/…List/…Value).[JsonPropertyName]keeps the wire format identical. Illegal identifiers (2fa,kebab-case, keywords) are sanitized.BlobRefunqualified —using ATProtoNet;/using ATProtoNet.Models;/using System.Text.Json;are emitted only when actually used, plus#nullable enableandusing System.Collections.Generic;.JsonElementfallbacks — optional members are always nullable, and unions/inline objects are now typed properly: a union of object defs emitsabstract class AttributionUnionwith[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]+[JsonDerivedType]variants (the variants subclass it); a union of tokens (profileType,businessTypein the profile schema) isstring; inline objects become nested classes (RecipeRecord.NutritionInfo,List<Ingredient>). Overlapping unions can't both use inheritance, so those still degrade toJsonElement?— and now say so as aWARN.SdkTypeMapmaps well-knowncom.atproto.*/app.bsky.*defs onto the SDK's models (app.bsky.embed.defs#aspectRatio→ATProtoNet.Lexicon.App.Bsky.Embed.AspectRatio). Refs that resolve to nothing becomeJsonElement?+ aWARNinstead of a dangling type name; a unit test reflects over the whole table to prove every mapped type exists.AtProtoRecord—public sealed class RecipeRecord : AtProtoRecord,createdAtinherited from the base.Alllist. The recipedefsdocument went from 101 generated classes to 9 (CookingMethod.Baking,Diet.Vegan,License.CreativeCommonsByNcSa, …); the family name is the longest prefix all members share, solicense*groups underLicenserather than fragmenting.Found while verifying (also fixed)
#appPassworddefs undercom.atproto.servercollided; later ones are prefixed with their document name.Bsky.Generated.Chat.Bsky.Actorshadowed qualified references toBsky.Generated.App.…; cross-namespace names are now rooted atglobal::."type"key —System.Text.Jsondoes not inherit[JsonPropertyName]onto an override, so the pattern inRecordCollection's XML docs, README, anddocs/(public override string Type => …with no attribute) wrote both"type"and"$type"into every record. The attribute is now repeated on the override in generated code, all doc examples, and the test fixtures, with a regression test.atprotoNSID segment now cases asAtProto(matching the SDK layout andNsidToNamespace's own documented behaviour), and"type": "number"→double.Verification
dotnet test tests/ATProtoNet.Tests/— 1010 passing, including 15 newCSharpEmitterTestsand the newAtProtoRecordserialization test.exchange.recipeschemas compiles against the SDK withTreatWarningsAsErrors=true, zero warnings, and round-trips:{"$type":"exchange.recipe.recipe",…,"attribution":{"$type":"exchange.recipe.defs#attributionWebsite",…}}deserializes back toAttributionWebsite. ZeroWARNlines for this corpus.WARNs, all honest reports of overlapping unions and def-name collisions).docs/lexicon-codegen.mdexample output was regenerated from the tool, so it matches byte-for-byte; CHANGELOG updated under[Unreleased].One thing left as-is: a Lexicon def used by two different unions can only inherit one base, so the second union stays
JsonElement?. Modelling that would need generated wrapper types per union instead of inheritance — worth a follow-up issue if Mise hits it.Sources: DNS lexicon authority, atproto lexicons, Lexicon spec
⏱ 26m 27s · 119 turns · tokens: 204.7k in / 106.2k out (+17077.1k cache-read)
📜 Full session transcript
→ Opened PR: #60