aboutsummaryrefslogtreecommitdiff
path: root/csharp/src/Google.Protobuf/Reflection/FieldDescriptor.cs
blob: 152467d802778958d600893fb77087052e77dc2c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
#region Copyright notice and license
// Protocol Buffers - Google's data interchange format
// Copyright 2008 Google Inc.  All rights reserved.
// https://developers.google.com/protocol-buffers/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//     * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#endregion

using Google.Protobuf.Compatibility;
using System;

namespace Google.Protobuf.Reflection
{
    /// <summary>
    /// Descriptor for a field or extension within a message in a .proto file.
    /// </summary>
    public sealed class FieldDescriptor : DescriptorBase, IComparable<FieldDescriptor>
    {
        private EnumDescriptor enumType;
        private MessageDescriptor messageType;
        private FieldType fieldType;
        private readonly string propertyName; // Annoyingly, needed in Crosslink.
        private IFieldAccessor accessor;

        /// <summary>
        /// Get the field's containing message type.
        /// </summary>
        public MessageDescriptor ContainingType { get; }

        /// <summary>
        /// Returns the oneof containing this field, or <c>null</c> if it is not part of a oneof.
        /// </summary>
        public OneofDescriptor ContainingOneof { get; }

        /// <summary>
        /// The effective JSON name for this field. This is usually the lower-camel-cased form of the field name,
        /// but can be overridden using the <c>json_name</c> option in the .proto file.
        /// </summary>
        public string JsonName { get; }

        internal FieldDescriptorProto Proto { get; }

        internal FieldDescriptor(FieldDescriptorProto proto, FileDescriptor file,
                                 MessageDescriptor parent, int index, string propertyName)
            : base(file, file.ComputeFullName(parent, proto.Name), index)
        {
            Proto = proto;
            if (proto.Type != 0)
            {
                fieldType = GetFieldTypeFromProtoType(proto.Type);
            }

            if (FieldNumber <= 0)
            {
                throw new DescriptorValidationException(this, "Field numbers must be positive integers.");
            }
            ContainingType = parent;
            // OneofIndex "defaults" to -1 due to a hack in FieldDescriptor.OnConstruction.
            if (proto.OneofIndex != -1)
            {
                if (proto.OneofIndex < 0 || proto.OneofIndex >= parent.Proto.OneofDecl.Count)
                {
                    throw new DescriptorValidationException(this,
                        $"FieldDescriptorProto.oneof_index is out of range for type {parent.Name}");
                }
                ContainingOneof = parent.Oneofs[proto.OneofIndex];
            }

            file.DescriptorPool.AddSymbol(this);
            // We can't create the accessor until we've cross-linked, unfortunately, as we
            // may not know whether the type of the field is a map or not. Remember the property name
            // for later.
            // We could trust the generated code and check whether the type of the property is
            // a MapField, but that feels a tad nasty.
            this.propertyName = propertyName;
            JsonName =  Proto.JsonName == "" ? JsonFormatter.ToJsonName(Proto.Name) : Proto.JsonName;
        }
    

        /// <summary>
        /// The brief name of the descriptor's target.
        /// </summary>
        public override string Name => Proto.Name;

        /// <summary>
        /// Returns the accessor for this field.
        /// </summary>
        /// <remarks>
        /// <para>
        /// While a <see cref="FieldDescriptor"/> describes the field, it does not provide
        /// any way of obtaining or changing the value of the field within a specific message;
        /// that is the responsibility of the accessor.
        /// </para>
        /// <para>
        /// In descriptors for generated code, the value returned by this property will be non-null for all
        /// regular fields. However, if a message containing a map field is introspected, the list of nested messages will include
        /// an auto-generated nested key/value pair message for the field. This is not represented in any
        /// generated type, and the value of the map field itself is represented by a dictionary in the
        /// reflection API. There are never instances of those "hidden" messages, so no accessor is provided
        /// and this property will return null.
        /// </para>
        /// <para>
        /// In dynamically loaded descriptors, the value returned by this property will current be null;
        /// if and when dynamic messages are supported, it will return a suitable accessor to work with
        /// them.
        /// </para>
        /// </remarks>
        public IFieldAccessor Accessor => accessor;
        
        /// <summary>
        /// Maps a field type as included in the .proto file to a FieldType.
        /// </summary>
        private static FieldType GetFieldTypeFromProtoType(FieldDescriptorProto.Types.Type type)
        {
            switch (type)
            {
                case FieldDescriptorProto.Types.Type.Double:
                    return FieldType.Double;
                case FieldDescriptorProto.Types.Type.Float:
                    return FieldType.Float;
                case FieldDescriptorProto.Types.Type.Int64:
                    return FieldType.Int64;
                case FieldDescriptorProto.Types.Type.Uint64:
                    return FieldType.UInt64;
                case FieldDescriptorProto.Types.Type.Int32:
                    return FieldType.Int32;
                case FieldDescriptorProto.Types.Type.Fixed64:
                    return FieldType.Fixed64;
                case FieldDescriptorProto.Types.Type.Fixed32:
                    return FieldType.Fixed32;
                case FieldDescriptorProto.Types.Type.Bool:
                    return FieldType.Bool;
                case FieldDescriptorProto.Types.Type.String:
                    return FieldType.String;
                case FieldDescriptorProto.Types.Type.Group:
                    return FieldType.Group;
                case FieldDescriptorProto.Types.Type.Message:
                    return FieldType.Message;
                case FieldDescriptorProto.Types.Type.Bytes:
                    return FieldType.Bytes;
                case FieldDescriptorProto.Types.Type.Uint32:
                    return FieldType.UInt32;
                case FieldDescriptorProto.Types.Type.Enum:
                    return FieldType.Enum;
                case FieldDescriptorProto.Types.Type.Sfixed32:
                    return FieldType.SFixed32;
                case FieldDescriptorProto.Types.Type.Sfixed64:
                    return FieldType.SFixed64;
                case FieldDescriptorProto.Types.Type.Sint32:
                    return FieldType.SInt32;
                case FieldDescriptorProto.Types.Type.Sint64:
                    return FieldType.SInt64;
                default:
                    throw new ArgumentException("Invalid type specified");
            }
        }

        /// <summary>
        /// Returns <c>true</c> if this field is a repeated field; <c>false</c> otherwise.
        /// </summary>
        public bool IsRepeated => Proto.Label == FieldDescriptorProto.Types.Label.Repeated;

        /// <summary>
        /// Returns <c>true</c> if this field is a map field; <c>false</c> otherwise.
        /// </summary>
        public bool IsMap => fieldType == FieldType.Message && messageType.Proto.Options != null && messageType.Proto.Options.MapEntry;

        /// <summary>
        /// Returns <c>true</c> if this field is a packed, repeated field; <c>false</c> otherwise.
        /// </summary>
        public bool IsPacked => 
            // Note the || rather than && here - we're effectively defaulting to packed, because that *is*
            // the default in proto3, which is all we support. We may give the wrong result for the protos
            // within descriptor.proto, but that's okay, as they're never exposed and we don't use IsPacked
            // within the runtime.
            Proto.Options == null || Proto.Options.Packed;
        
        /// <summary>
        /// Returns the type of the field.
        /// </summary>
        public FieldType FieldType => fieldType;

        /// <summary>
        /// Returns the field number declared in the proto file.
        /// </summary>
        public int FieldNumber => Proto.Number;

        /// <summary>
        /// Compares this descriptor with another one, ordering in "canonical" order
        /// which simply means ascending order by field number. <paramref name="other"/>
        /// must be a field of the same type, i.e. the <see cref="ContainingType"/> of
        /// both fields must be the same.
        /// </summary>
        public int CompareTo(FieldDescriptor other)
        {
            if (other.ContainingType != ContainingType)
            {
                throw new ArgumentException("FieldDescriptors can only be compared to other FieldDescriptors " +
                                            "for fields of the same message type.");
            }
            return FieldNumber - other.FieldNumber;
        }

        /// <summary>
        /// For enum fields, returns the field's type.
        /// </summary>
        public EnumDescriptor EnumType
        {
            get
            {
                if (fieldType != FieldType.Enum)
                {
                    throw new InvalidOperationException("EnumType is only valid for enum fields.");
                }
                return enumType;
            }
        }

        /// <summary>
        /// For embedded message and group fields, returns the field's type.
        /// </summary>
        public MessageDescriptor MessageType
        {
            get
            {
                if (fieldType != FieldType.Message)
                {
                    throw new InvalidOperationException("MessageType is only valid for message fields.");
                }
                return messageType;
            }
        }

        /// <summary>
        /// The (possibly empty) set of custom options for this field.
        /// </summary>
        public CustomOptions CustomOptions => Proto.Options?.CustomOptions ?? CustomOptions.Empty;

        /// <summary>
        /// Look up and cross-link all field types etc.
        /// </summary>
        internal void CrossLink()
        {
            if (Proto.TypeName != "")
            {
                IDescriptor typeDescriptor =
                    File.DescriptorPool.LookupSymbol(Proto.TypeName, this);

                if (Proto.Type != 0)
                {
                    // Choose field type based on symbol.
                    if (typeDescriptor is MessageDescriptor)
                    {
                        fieldType = FieldType.Message;
                    }
                    else if (typeDescriptor is EnumDescriptor)
                    {
                        fieldType = FieldType.Enum;
                    }
                    else
                    {
                        throw new DescriptorValidationException(this, $"\"{Proto.TypeName}\" is not a type.");
                    }
                }

                if (fieldType == FieldType.Message)
                {
                    if (!(typeDescriptor is MessageDescriptor))
                    {
                        throw new DescriptorValidationException(this, $"\"{Proto.TypeName}\" is not a message type.");
                    }
                    messageType = (MessageDescriptor) typeDescriptor;

                    if (Proto.DefaultValue != "")
                    {
                        throw new DescriptorValidationException(this, "Messages can't have default values.");
                    }
                }
                else if (fieldType == FieldType.Enum)
                {
                    if (!(typeDescriptor is EnumDescriptor))
                    {
                        throw new DescriptorValidationException(this, $"\"{Proto.TypeName}\" is not an enum type.");
                    }
                    enumType = (EnumDescriptor) typeDescriptor;
                }
                else
                {
                    throw new DescriptorValidationException(this, "Field with primitive type has type_name.");
                }
            }
            else
            {
                if (fieldType == FieldType.Message || fieldType == FieldType.Enum)
                {
                    throw new DescriptorValidationException(this, "Field with message or enum type missing type_name.");
                }
            }

            // Note: no attempt to perform any default value parsing

            File.DescriptorPool.AddFieldByNumber(this);

            if (ContainingType != null && ContainingType.Proto.Options != null && ContainingType.Proto.Options.MessageSetWireFormat)
            {
                throw new DescriptorValidationException(this, "MessageSet format is not supported.");
            }
            accessor = CreateAccessor();
        }

        private IFieldAccessor CreateAccessor()
        {
            // If we're given no property name, that's because we really don't want an accessor.
            // This could be because it's a map message, or it could be that we're loading a FileDescriptor dynamically.
            // TODO: Support dynamic messages.
            if (propertyName == null)
            {
                return null;
            }
            var property = ContainingType.ClrType.GetProperty(propertyName);
            if (property == null)
            {
                throw new DescriptorValidationException(this, $"Property {propertyName} not found in {ContainingType.ClrType}");
            }
            return IsMap ? new MapFieldAccessor(property, this)
                : IsRepeated ? new RepeatedFieldAccessor(property, this)
                : (IFieldAccessor) new SingleFieldAccessor(property, this);
        }
    }
}