コード例 #1
0
        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)
                    {