Skip to content

Commit

Permalink
Added JSON root records feature.
Browse files Browse the repository at this point in the history
  • Loading branch information
clemensv committed Aug 23, 2024
1 parent 72114de commit 542a874
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 31 deletions.
60 changes: 33 additions & 27 deletions lang/csharp/src/apache/main/IO/Parsing/JsonGrammarGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,38 +86,44 @@ protected override Symbol Generate(Schema sc, IDictionary<LitS, Symbol> seen)
LitS wsc = new LitS(sc);
if (!seen.TryGetValue(wsc, out Symbol rresult))
{
Symbol[] production = new Symbol[((RecordSchema)sc).Fields.Count * 3 + 2];
rresult = Symbol.NewSeq(production);
seen[wsc] = rresult;

int i = production.Length;
int n = 0;
production[--i] = Symbol.RecordStart;
foreach (Field f in ((RecordSchema)sc).Fields)
RecordSchema recordSchema = (RecordSchema)sc;
if (recordSchema.TryGetRootField(out var rootField))
{
var name = f.Name;
if (f.AlternateNames != null && f.AlternateNames.TryGetValue("json", out string jsonName))
{
name = jsonName;
}
production[--i] = new Symbol.FieldAdjustAction(n, name, f.Aliases);
string constValue = f.GetProperty("const");
if (constValue != null)
{
var constObj = JsonConvert.DeserializeObject(constValue);
production[--i] = Symbol.NewSeq(new Symbol.ConstCheckAction(constObj), Generate(f.Schema, seen));
}
else
return Generate(rootField.Schema, seen);
}
else
{
Symbol[] production = new Symbol[recordSchema.Fields.Count * 3 + 2];
rresult = Symbol.NewSeq(production);
seen[wsc] = rresult;

int i = production.Length;
int n = 0;
production[--i] = Symbol.RecordStart;
foreach (Field f in recordSchema.Fields)
{
production[--i] = Generate(f.Schema, seen);
var name = f.Name;
if (f.AlternateNames != null && f.AlternateNames.TryGetValue("json", out string jsonName))
{
name = jsonName;
}
production[--i] = new Symbol.FieldAdjustAction(n, name, f.Aliases);
string constValue = f.GetProperty("const");
if (constValue != null)
{
var constObj = JsonConvert.DeserializeObject(constValue);
production[--i] = Symbol.NewSeq(new Symbol.ConstCheckAction(constObj), Generate(f.Schema, seen));
}
else
{
production[--i] = Generate(f.Schema, seen);
}
production[--i] = Symbol.FieldEnd;
n++;
}
production[--i] = Symbol.FieldEnd;
n++;
production[i - 1] = Symbol.RecordEnd;
}

production[i - 1] = Symbol.RecordEnd;
}

return rresult;
}
case Schema.Type.Logical:
Expand Down
24 changes: 21 additions & 3 deletions lang/csharp/src/apache/main/Schema/Field.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ public enum SortOrder
/// </summary>
public IDictionary<string, string> AlternateNames { get; private set; }

/// <summary>
/// Whether the field is a root field. There must only be one such field in a record.
/// </summary>
public bool Root { get; private set; }

/// <summary>
/// List of aliases for the field name.
/// </summary>
Expand Down Expand Up @@ -119,11 +124,12 @@ public Field(Schema schema,
int pos,
IList<string> aliases = null,
IDictionary<string, string> alternateNames = null,
bool root = false,

Check warning on line 127 in lang/csharp/src/apache/main/Schema/Field.cs

View workflow job for this annotation

GitHub Actions / interop

Parameter 'root' has no matching param tag in the XML comment for 'Field.Field(Schema, string, int, IList<string>, IDictionary<string, string>, bool, string, JToken, Field.SortOrder, PropertyMap)' (but other parameters do)

Check warning on line 127 in lang/csharp/src/apache/main/Schema/Field.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Parameter 'root' has no matching param tag in the XML comment for 'Field.Field(Schema, string, int, IList<string>, IDictionary<string, string>, bool, string, JToken, Field.SortOrder, PropertyMap)' (but other parameters do)

Check warning on line 127 in lang/csharp/src/apache/main/Schema/Field.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Parameter 'root' has no matching param tag in the XML comment for 'Field.Field(Schema, string, int, IList<string>, IDictionary<string, string>, bool, string, JToken, Field.SortOrder, PropertyMap)' (but other parameters do)

Check warning on line 127 in lang/csharp/src/apache/main/Schema/Field.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Parameter 'root' has no matching param tag in the XML comment for 'Field.Field(Schema, string, int, IList<string>, IDictionary<string, string>, bool, string, JToken, Field.SortOrder, PropertyMap)' (but other parameters do)

Check warning on line 127 in lang/csharp/src/apache/main/Schema/Field.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Parameter 'root' has no matching param tag in the XML comment for 'Field.Field(Schema, string, int, IList<string>, IDictionary<string, string>, bool, string, JToken, Field.SortOrder, PropertyMap)' (but other parameters do)

Check failure on line 127 in lang/csharp/src/apache/main/Schema/Field.cs

View workflow job for this annotation

GitHub Actions / test

Parameter 'root' has no matching param tag in the XML comment for 'Field.Field(Schema, string, int, IList<string>, IDictionary<string, string>, bool, string, JToken, Field.SortOrder, PropertyMap)' (but other parameters do)

Check failure on line 127 in lang/csharp/src/apache/main/Schema/Field.cs

View workflow job for this annotation

GitHub Actions / test

Parameter 'root' has no matching param tag in the XML comment for 'Field.Field(Schema, string, int, IList<string>, IDictionary<string, string>, bool, string, JToken, Field.SortOrder, PropertyMap)' (but other parameters do)

Check failure on line 127 in lang/csharp/src/apache/main/Schema/Field.cs

View workflow job for this annotation

GitHub Actions / test

Parameter 'root' has no matching param tag in the XML comment for 'Field.Field(Schema, string, int, IList<string>, IDictionary<string, string>, bool, string, JToken, Field.SortOrder, PropertyMap)' (but other parameters do)

Check failure on line 127 in lang/csharp/src/apache/main/Schema/Field.cs

View workflow job for this annotation

GitHub Actions / test

Parameter 'root' has no matching param tag in the XML comment for 'Field.Field(Schema, string, int, IList<string>, IDictionary<string, string>, bool, string, JToken, Field.SortOrder, PropertyMap)' (but other parameters do)

Check failure on line 127 in lang/csharp/src/apache/main/Schema/Field.cs

View workflow job for this annotation

GitHub Actions / test

Parameter 'root' has no matching param tag in the XML comment for 'Field.Field(Schema, string, int, IList<string>, IDictionary<string, string>, bool, string, JToken, Field.SortOrder, PropertyMap)' (but other parameters do)

Check failure on line 127 in lang/csharp/src/apache/main/Schema/Field.cs

View workflow job for this annotation

GitHub Actions / test

Parameter 'root' has no matching param tag in the XML comment for 'Field.Field(Schema, string, int, IList<string>, IDictionary<string, string>, bool, string, JToken, Field.SortOrder, PropertyMap)' (but other parameters do)

Check failure on line 127 in lang/csharp/src/apache/main/Schema/Field.cs

View workflow job for this annotation

GitHub Actions / test

Parameter 'root' has no matching param tag in the XML comment for 'Field.Field(Schema, string, int, IList<string>, IDictionary<string, string>, bool, string, JToken, Field.SortOrder, PropertyMap)' (but other parameters do)

Check failure on line 127 in lang/csharp/src/apache/main/Schema/Field.cs

View workflow job for this annotation

GitHub Actions / test

Parameter 'root' has no matching param tag in the XML comment for 'Field.Field(Schema, string, int, IList<string>, IDictionary<string, string>, bool, string, JToken, Field.SortOrder, PropertyMap)' (but other parameters do)

Check warning on line 127 in lang/csharp/src/apache/main/Schema/Field.cs

View workflow job for this annotation

GitHub Actions / interop

Parameter 'root' has no matching param tag in the XML comment for 'Field.Field(Schema, string, int, IList<string>, IDictionary<string, string>, bool, string, JToken, Field.SortOrder, PropertyMap)' (but other parameters do)
string doc = null,
JToken defaultValue = null,
SortOrder sortorder = SortOrder.ignore,
PropertyMap customProperties = null)
: this(schema, name, aliases, alternateNames, pos, doc, defaultValue, sortorder, customProperties)
: this(schema, name, aliases, alternateNames, root, pos, doc, defaultValue, sortorder, customProperties)
{
}

Expand All @@ -133,7 +139,7 @@ public Field(Schema schema,
/// <returns>A clone of this field with new position.</returns>
internal Field ChangePosition(int newPosition)
{
return new Field(Schema, Name, newPosition, Aliases, AlternateNames, Documentation, DefaultValue, Ordering ?? SortOrder.ignore, Props);
return new Field(Schema, Name, newPosition, Aliases, AlternateNames, Root, Documentation, DefaultValue, Ordering ?? SortOrder.ignore, Props);
}

/// <summary>
Expand All @@ -143,6 +149,7 @@ internal Field ChangePosition(int newPosition)
/// <param name="name">name of the field</param>
/// <param name="aliases">list of aliases for the name of the field</param>
/// <param name="alternateNames">dictionary of alternate names for the field</param>
/// <param name="root">whether the field is a root field. There must only be one such field in a record</param>
/// <param name="pos">position of the field</param>
/// <param name="doc">documentation for the field</param>
/// <param name="defaultValue">field's default value if it exists</param>
Expand All @@ -153,7 +160,7 @@ internal Field ChangePosition(int newPosition)
/// or
/// type - type cannot be null.
/// </exception>
internal Field(Schema schema, string name, IList<string> aliases, IDictionary<string, string> alternateNames, int pos, string doc,
internal Field(Schema schema, string name, IList<string> aliases, IDictionary<string, string> alternateNames, bool root, int pos, string doc,
JToken defaultValue, SortOrder sortorder, PropertyMap props)
{
if (string.IsNullOrEmpty(name))
Expand All @@ -164,11 +171,17 @@ internal Field(Schema schema, string name, IList<string> aliases, IDictionary<st
Schema = schema ?? throw new ArgumentNullException("type", "type cannot be null.");
Name = name;
Aliases = aliases;
AlternateNames = alternateNames;
Root = root;
Pos = pos;
Documentation = doc;
DefaultValue = defaultValue;
Ordering = sortorder;
Props = props;
if ( Root && (Schema.Tag != Schema.Type.Array && Schema.Tag != Schema.Type.Map) )
{
throw new AvroTypeException("Field labeled 'root' must be of type array or map.");
}
}

/// <summary>
Expand Down Expand Up @@ -219,6 +232,11 @@ protected internal void writeJson(JsonTextWriter writer, SchemaNames names, stri
}
writer.WriteEndObject();
}
if (Root)
{
writer.WritePropertyName("root");
writer.WriteValue(Root);
}

writer.WriteEndObject();
}
Expand Down
20 changes: 19 additions & 1 deletion lang/csharp/src/apache/main/Schema/RecordSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,21 @@ public List<Field> Fields
/// </summary>
public int Count { get { return Fields.Count; } }

/// <summary>
/// Whether the record has a root field
/// </summary>
public bool HasRootField => Fields?.Any(f => f.Root)??false;

/// <summary>
/// Gets the root field of the record, if any
/// </summary>
/// <returns>Root field schema</returns>
public bool TryGetRootField(out Field field)
{
field = Fields?.FirstOrDefault(f => f.Root);
return field != null;
}

/// <summary>
/// Map of field name and Field object for faster field lookups
/// </summary>
Expand Down Expand Up @@ -276,18 +291,21 @@ private static Field createField(JToken jfield, int pos, SchemaNames names, stri
var alternateNames = Field.GetAlternateNames(jfield);
var props = Schema.GetProperties(jfield);
var defaultValue = jfield["default"];
var root = JsonHelper.GetOptionalBoolean(jfield, "root")??false;

JToken jtype = jfield["type"];
if (null == jtype)
throw new SchemaParseException($"'type' was not found for field: name at '{jfield.Path}'");
var schema = Schema.ParseJson(jtype, names, encspace);
return new Field(schema, name, aliases, alternateNames, pos, doc, defaultValue, sortorder, props);
return new Field(schema, name, aliases, alternateNames, root, pos, doc, defaultValue, sortorder, props);
}

private static void addToFieldMap(Dictionary<string, Field> map, string name, Field field)
{
if (map.ContainsKey(name))
throw new AvroException("field or alias " + name + " is a duplicate name");
if ((field.Root && map.Count > 0) || map.Values.Any(f => f.Root))
throw new AvroException("When 'root': true is set on a field, no other fields may exist");
map.Add(name, field);
}

Expand Down
52 changes: 52 additions & 0 deletions lang/csharp/src/apache/test/IO/JsonCodecTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,8 @@ public void TestJsonRecordOrderingWithProjection2()
}
}



[TestCase("{\"int\":123}")]
[TestCase("{\"string\":\"12345678-1234-5678-1234-123456789012\"}")]
[TestCase("null")]
Expand Down Expand Up @@ -659,6 +661,56 @@ private void AssertEquivalent(string json1, string json2)
{
Assert.IsTrue(JToken.DeepEquals(JToken.Parse(json1), JToken.Parse(json2)));
}

[Test]
public void TestRootFeatureWithArray()
{
string schemaStr = "{ \"type\": \"record\", \"name\": \"r\", \"fields\": [ " +
"{ \"name\" : \"rootField\", \"type\": { \"type\": \"array\", \"items\": \"int\" }, \"root\": true } " +
"] }";
string json = "[1,2,3]";

Schema schema = Schema.Parse(schemaStr);
byte[] avroBytes = fromJsonToAvro(json, schema, JsonMode.PlainJson);
Assert.IsNotNull(avroBytes);
string convertedJson = fromAvroToJson(avroBytes, schema, false, JsonMode.PlainJson);
Assert.AreEqual(json, convertedJson);
}

[Test]
public void TestRootFeatureWithMap()
{
string schemaStr = "{ \"type\": \"record\", \"name\": \"r\", \"fields\": [ " +
"{ \"name\" : \"rootField\", \"type\": { \"type\": \"map\", \"values\": \"string\" }, \"root\": true } " +
"] }";
string json = "{\"key1\":\"value1\",\"key2\":\"value2\"}";

Schema schema = Schema.Parse(schemaStr);
byte[] avroBytes = fromJsonToAvro(json, schema, JsonMode.PlainJson);
Assert.IsNotNull(avroBytes);
string convertedJson = fromAvroToJson(avroBytes, schema, false, JsonMode.PlainJson);
Assert.AreEqual(json, convertedJson);
}

[Test]
public void TestRootFeatureWithMultipleRootFields()
{
string schemaStr = "{ \"type\": \"record\", \"name\": \"r\", \"fields\": [ " +
"{ \"name\" : \"rootField1\", \"type\": { \"type\": \"array\", \"items\": \"int\" }, \"root\": true }, " +
"{ \"name\" : \"rootField2\", \"type\": { \"type\": \"map\", \"values\": \"string\" }, \"root\": true } " +
"] }";
Assert.Throws<SchemaParseException>(() => Schema.Parse(schemaStr));
}

[Test]
public void TestRootFeatureWithAdditionalField()
{
string schemaStr = "{ \"type\": \"record\", \"name\": \"r\", \"fields\": [ " +
"{ \"name\" : \"rootField\", \"type\": { \"type\": \"array\", \"items\": \"int\" }, \"root\": true }, " +
"{ \"name\" : \"additionalField\", \"type\": \"string\" } " +
"] }";
Assert.Throws<SchemaParseException>(() => Schema.Parse(schemaStr));
}
}

public partial class Root : global::Avro.Specific.ISpecificRecord
Expand Down
4 changes: 4 additions & 0 deletions lang/csharp/src/apache/test/Schema/SchemaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ public void TestRecordCreation(string expectedSchema, string name, string space,
fieldName,
fieldsAliases[i] == null? null: new List<string> { fieldsAliases[i] },
null,
false,
i,
fieldsDocs[i],
fieldsDefaultValues[i].ToString(),
Expand Down Expand Up @@ -348,6 +349,7 @@ public void TestRecordCreationWithDuplicateFields()
"value",
new List<string> { "oldName" },
null,
false,
0,
null,
"100",
Expand All @@ -370,6 +372,7 @@ public void TestRecordFieldNames() {
"歳以上",
null,
null,
false,
0,
null,
null,
Expand All @@ -395,6 +398,7 @@ public void TestRecordCreationWithRecursiveRecord()
"value",
null,
null,
false,
0,
null,
null,
Expand Down

0 comments on commit 542a874

Please sign in to comment.