bool ExtendPocoClass(IActivityMonitor monitor, IPocoRootInfo pocoInfo, JsonSerializationCodeGen jsonCodeGen, ITypeScope pocoClass) { bool success = true; // Each Poco class is a IWriter and has a constructor that accepts a Utf8JsonReader. pocoClass.Definition.BaseTypes.Add(new ExtendedTypeName("CK.Core.PocoJsonSerializer.IWriter")); // Defines ToString() to return the Json representation only if it is not already defined. var toString = FunctionDefinition.Parse("public override string ToString()"); if (pocoClass.FindFunction(toString.Key, false) == null) { pocoClass .CreateFunction(toString) .GeneratedByComment().NewLine() .Append("var m = new System.Buffers.ArrayBufferWriter<byte>();").NewLine() .Append("using( var w = new System.Text.Json.Utf8JsonWriter( m ) )").NewLine() .OpenBlock() .Append("Write( w, false, null );").NewLine() .Append("w.Flush();").NewLine() .CloseBlock() .Append("return Encoding.UTF8.GetString( m.WrittenMemory.Span );"); } // The Write method: // - The writeHeader part may contain the ECMAScriptStandard non compliant exception (if it appears that a UnionType is not compliant). // - The write part will be filled with the properties (name and writer code). pocoClass.Append("public void Write( System.Text.Json.Utf8JsonWriter w, bool withType, PocoJsonSerializerOptions options )") .OpenBlock() .GeneratedByComment().NewLine() .CreatePart(out var writeHeader) .Append("bool usePascalCase = options != null && options.ForJsonSerializer.PropertyNamingPolicy != System.Text.Json.JsonNamingPolicy.CamelCase;").NewLine() .Append("if( withType ) { w.WriteStartArray(); w.WriteStringValue( ").AppendSourceString(pocoInfo.Name).Append("); }").NewLine() .Append("w.WriteStartObject();") .CreatePart(out var write) .Append("w.WriteEndObject();").NewLine() .Append("if( withType ) w.WriteEndArray();").NewLine() .CloseBlock(); // The constructor calls the private Read method. pocoClass.Append("public ").Append(pocoClass.Name).Append("( ref System.Text.Json.Utf8JsonReader r, PocoJsonSerializerOptions options ) : this()") .OpenBlock() .Append("Read( ref r, options );") .CloseBlock(); // Poco has a Read method but it is not (currently) exposed. // This returns two parts: a header (to inject the ECMAScriptStandard non compliant // exception if it appears that a UnionType is not compliant) and the switch-case part on the // property names with their reader code. var(readHeader, read) = GenerateReadBody(pocoInfo, pocoClass); bool isECMAScriptStandardCompliant = true; var jsonProperties = new PocoJsonPropertyInfo[pocoInfo.PropertyList.Count]; foreach (var p in pocoInfo.PropertyList) { var mainHandler = jsonCodeGen.GetHandler(p.PropertyNullableTypeTree); if (mainHandler == null) { success = false; continue; } PocoJsonPropertyInfo?pJ; if (p.PropertyUnionTypes.Any()) { pJ = HandleUnionType(p, monitor, jsonCodeGen, write, read, ref isECMAScriptStandardCompliant, mainHandler); if (pJ == null) { success = false; break; } } else { var handlers = new[] { mainHandler }; pJ = new PocoJsonPropertyInfo(p, handlers, mainHandler.HasECMAScriptStandardJsonName && isECMAScriptStandardCompliant ? handlers : null); // Actual Read/Write generation cannot be done here (it must be postponed). // This loop registers/allows the poco property types (the call to GetHandler triggers // the type registration) but writing them requires to know whether those types are final or not . // We store (using closure) the property, the write and read parts and the handler(s) // (to avoid another lookup) and wait for the FinalizeJsonSupport to be called. _finalReadWrite.Add(() => { var fieldName = "_v" + p.Index; write.Append("w.WritePropertyName( usePascalCase ? ") .AppendSourceString(p.PropertyName) .Append(" : ") .AppendSourceString(System.Text.Json.JsonNamingPolicy.CamelCase.ConvertName(p.PropertyName)) .Append(" );").NewLine(); mainHandler.GenerateWrite(write, fieldName); write.NewLine(); var camel = System.Text.Json.JsonNamingPolicy.CamelCase.ConvertName(p.PropertyName); if (camel != p.PropertyName) { read.Append("case ").AppendSourceString(camel).Append(": "); } read.Append("case ").AppendSourceString(p.PropertyName).Append(": ") .OpenBlock(); mainHandler.GenerateRead(read, fieldName, assignOnly: !p.IsReadOnly); read.Append("break; ") .CloseBlock(); }); } p.AddAnnotation(pJ); jsonProperties[p.Index] = pJ; } if (success) { if (!isECMAScriptStandardCompliant) { writeHeader.And(readHeader).Append("if( options != null && options.Mode == PocoJsonSerializerMode.ECMAScriptStandard ) throw new NotSupportedException( \"Poco '") .Append(pocoInfo.Name) .Append("' is not compliant with the ECMAScripStandard mode.\" );").NewLine(); } var info = new PocoJsonInfo(pocoInfo, isECMAScriptStandardCompliant, jsonProperties); pocoInfo.AddAnnotation(info); } return(success); }
PocoJsonPropertyInfo?HandleUnionType(IPocoPropertyInfo p, IActivityMonitor monitor, JsonSerializationCodeGen jsonCodeGen, ITypeScopePart write, ITypeScopePart read, ref bool isPocoECMAScriptStandardCompliant, JsonCodeGenHandler mainHandler) { // Analyses the UnionTypes and creates the handler for each of them. // - Forbids ambiguous mapping for ECMAScriptStandard: all numerics are mapped to "Number" or "BigInt" (and arrays or lists are arrays). // - The ECMAScriptStandard projected name must be unique (and is associated to its actual handler). // - var allHandlers = new List <JsonCodeGenHandler>(); var checkDuplicatedStandardName = new Dictionary <string, List <JsonCodeGenHandler> >(); // Gets all the handlers and build groups of ECMAStandardJsnoName handlers (only if the Poco is still standard compliant). foreach (var union in p.PropertyUnionTypes) { var h = jsonCodeGen.GetHandler(union); if (h == null) { return(null); } allHandlers.Add(h); if (isPocoECMAScriptStandardCompliant && h.HasECMAScriptStandardJsonName) { var n = h.ECMAScriptStandardJsonName; if (checkDuplicatedStandardName.TryGetValue(n.Name, out var exists)) { exists.Add(h); } else { checkDuplicatedStandardName.Add(n.Name, new List <JsonCodeGenHandler>() { h }); } } } allHandlers.Sort((h1, h2) => h1.TypeInfo.TypeSpecOrder.CompareTo(h2.TypeInfo.TypeSpecOrder)); // Analyze the groups (only if the Poco is still standard compliant). List <JsonCodeGenHandler>?ecmaStandardReadhandlers = null; bool isECMAScriptStandardCompliant = isPocoECMAScriptStandardCompliant; if (isECMAScriptStandardCompliant) { foreach (var group in checkDuplicatedStandardName.Values) { if (group.Count > 1) { int idxCanocical = group.IndexOf(h => h.ECMAScriptStandardJsonName.IsCanonical); if (idxCanocical == -1) { monitor.Warn($"{p} UnionType '{group.Select( h => h.GenCSharpName ).Concatenate( "' ,'" )}' types mapped to the same ECMAScript standard name: '{group[0].ECMAScriptStandardJsonName.Name}' and none of them is the 'Canonical' form. De/serializing this Poco in 'ECMAScriptstandard' will throw a NotSupportedException."); isECMAScriptStandardCompliant = false; break; } var winner = group[idxCanocical]; monitor.Trace($"{p} UnionType '{group.Select( h => h.GenCSharpName ).Concatenate( "' ,'" )}' types will be read as {winner.GenCSharpName} in ECMAScript standard name."); if (ecmaStandardReadhandlers == null) { ecmaStandardReadhandlers = allHandlers.Where(h => !h.HasECMAScriptStandardJsonName).ToList(); } ecmaStandardReadhandlers.Add(winner); } else { monitor.Debug($"{p} UnionType unambiguous mapping in ECMAScript standard name from '{group[0].ECMAScriptStandardJsonName.Name}' to '{group[0].GenCSharpName}'."); if (ecmaStandardReadhandlers == null) { ecmaStandardReadhandlers = allHandlers.Where(h => !h.HasECMAScriptStandardJsonName).ToList(); } ecmaStandardReadhandlers.Add(group[0]); } } isPocoECMAScriptStandardCompliant &= isECMAScriptStandardCompliant; } // Invariant: handlers are by design associated to different "oblivious NRT" types: switch case can be done on them. // That means that the actual's object type is enough to identify the exact handler (in THE CONTEXT of this property type!). // And the property setter controls the assignation: the set of types is controlled. // // It is tempting to simply call the generic write function but this one uses the GenericWriteHandler that is the "oblivious NRT" type: // even if this union exposes a ISet<string>? (nullable of non-nullable), it will be a ISet<string?> (non-nullable - since the GenericWriteHandler // is by design a NonNullHandler - of nullable - that is the oblivious nullability for reference type) that will be serialized. // // Actually the type name doesn't really matter, it's just a convention that a client must follow to receive or send data: here, we could // perfectly use the index of the type in the union types, that would be an identifier "local to this property" but this would do the job. // // What really matters is to identify the function that will read the data with the right null handling so that no nulls can be // injected where it should not AND to use the right function to write the data, the one that will not let unexpected nulls emitted. // Regarding this, using the GenericWriteHandler is definitely not right. // // That's why we generate a dedicated switch-case for writing here. If one of the handler is bound to the ObjectType (currently // that's true when jsonTypeInfo.IsFinal is false), we call the generic write object in the default: case. // Debug.Assert(!allHandlers.Select(h => h.TypeInfo.GenCSharpName).GroupBy(Util.FuncIdentity).Any(g => g.Count() > 1)); var info = new PocoJsonPropertyInfo(p, allHandlers, isECMAScriptStandardCompliant ? ecmaStandardReadhandlers : null); _finalReadWrite.Add(() => { var fieldName = "_v" + info.PropertyInfo.Index; write.Append("w.WritePropertyName( ").AppendSourceString(info.PropertyInfo.PropertyName).Append(" );").NewLine(); if (info.IsJsonUnionType) { write.GeneratedByComment() .Append(@"switch( ").Append(fieldName).Append(" )") .OpenBlock() .Append("case null: "); if (info.PropertyInfo.IsNullable) { write.Append("w.WriteNullValue();").NewLine() .Append("break;"); } else { write.Append(@"throw new InvalidOperationException( ""A null value appear where it should not. Writing JSON is impossible."" );"); } write.NewLine(); bool hasDefaultObject = false; foreach (var h in info.AllHandlers) { Debug.Assert(!h.IsNullable, "Union types are not nullable by design (null has been generalized)."); if (h.TypeInfo.IsFinal) { write.Append("case ").Append(h.TypeInfo.MostAbstractMapping?.GenCSharpName ?? h.TypeInfo.GenCSharpName).Append(" v: ").NewLine(); h.DoGenerateWrite(write, "v", handleNull: false, writeTypeName: true); write.NewLine().Append("break;").NewLine(); } else { hasDefaultObject = true; } } write.Append(@"default:").NewLine(); if (hasDefaultObject) { mainHandler.ToNonNullHandler().GenerateWrite(write, fieldName); write.NewLine().Append("break;"); } else { write.Append(@"throw new InvalidOperationException( $""Unexpected type {").Append(fieldName).Append(@".GetType()} in union ").Append(info.PropertyInfo.ToString() !).Append(@"."" );"); } write.CloseBlock(); } else { info.AllHandlers[0].GenerateWrite(write, fieldName); } read.Append("case ").AppendSourceString(info.PropertyInfo.PropertyName).Append(": ") .OpenBlock(); if (info.IsJsonUnionType) { read.Append("if( r.TokenType == System.Text.Json.JsonTokenType.Null )"); if (info.PropertyInfo.IsNullable) { read.OpenBlock() .Append(fieldName).Append(" = null;").NewLine() .Append("r.Read();") .CloseBlock() .Append("else") .OpenBlock(); } else { read.Append(" throw new System.Text.Json.JsonException(\"").Append(info.PropertyInfo.ToString() !).Append(" cannot be null.\");").NewLine(); } if (info.IsJsonUnionType) {