Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
P
protobuf
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Packages
Packages
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
submodule
protobuf
Commits
e4af879b
Commit
e4af879b
authored
9 years ago
by
Jon Skeet
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1000 from jskeet/any-format
JSON handling for Any
parents
bdabaeb0
3de2fced
No related merge requests found
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
198 additions
and
26 deletions
+198
-26
JsonFormatterTest.cs
csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs
+42
-0
JsonParserTest.cs
csharp/src/Google.Protobuf.Test/JsonParserTest.cs
+50
-0
JsonTokenizerTest.cs
csharp/src/Google.Protobuf.Test/JsonTokenizerTest.cs
+7
-7
JsonFormatter.cs
csharp/src/Google.Protobuf/JsonFormatter.cs
+99
-19
JsonParser.cs
csharp/src/Google.Protobuf/JsonParser.cs
+0
-0
JsonTokenizer.cs
csharp/src/Google.Protobuf/JsonTokenizer.cs
+0
-0
No files found.
csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs
View file @
e4af879b
...
...
@@ -35,6 +35,7 @@ using Google.Protobuf.TestProtos;
using
NUnit.Framework
;
using
UnitTest.Issues.TestProtos
;
using
Google.Protobuf.WellKnownTypes
;
using
Google.Protobuf.Reflection
;
namespace
Google.Protobuf
{
...
...
@@ -420,6 +421,47 @@ namespace Google.Protobuf
AssertJson
(
"{ 'fileName': 'foo.proto' }"
,
JsonFormatter
.
Default
.
Format
(
message
));
}
[
Test
]
public
void
AnyWellKnownType
()
{
var
formatter
=
new
JsonFormatter
(
new
JsonFormatter
.
Settings
(
false
,
TypeRegistry
.
FromMessages
(
Timestamp
.
Descriptor
)));
var
timestamp
=
new
DateTime
(
1673
,
6
,
19
,
12
,
34
,
56
,
DateTimeKind
.
Utc
).
ToTimestamp
();
var
any
=
Any
.
Pack
(
timestamp
);
AssertJson
(
"{ '@type': 'type.googleapis.com/google.protobuf.Timestamp', 'value': '1673-06-19T12:34:56Z' }"
,
formatter
.
Format
(
any
));
}
[
Test
]
public
void
AnyMessageType
()
{
var
formatter
=
new
JsonFormatter
(
new
JsonFormatter
.
Settings
(
false
,
TypeRegistry
.
FromMessages
(
TestAllTypes
.
Descriptor
)));
var
message
=
new
TestAllTypes
{
SingleInt32
=
10
,
SingleNestedMessage
=
new
TestAllTypes
.
Types
.
NestedMessage
{
Bb
=
20
}
};
var
any
=
Any
.
Pack
(
message
);
AssertJson
(
"{ '@type': 'type.googleapis.com/protobuf_unittest.TestAllTypes', 'singleInt32': 10, 'singleNestedMessage': { 'bb': 20 } }"
,
formatter
.
Format
(
any
));
}
[
Test
]
public
void
AnyNested
()
{
var
registry
=
TypeRegistry
.
FromMessages
(
TestWellKnownTypes
.
Descriptor
,
TestAllTypes
.
Descriptor
);
var
formatter
=
new
JsonFormatter
(
new
JsonFormatter
.
Settings
(
false
,
registry
));
// Nest an Any as the value of an Any.
var
doubleNestedMessage
=
new
TestAllTypes
{
SingleInt32
=
20
};
var
nestedMessage
=
Any
.
Pack
(
doubleNestedMessage
);
var
message
=
new
TestWellKnownTypes
{
AnyField
=
Any
.
Pack
(
nestedMessage
)
};
AssertJson
(
"{ 'anyField': { '@type': 'type.googleapis.com/google.protobuf.Any', 'value': { '@type': 'type.googleapis.com/protobuf_unittest.TestAllTypes', 'singleInt32': 20 } } }"
,
formatter
.
Format
(
message
));
}
[
Test
]
public
void
AnyUnknownType
()
{
// The default type registry doesn't have any types in it.
var
message
=
new
TestAllTypes
();
var
any
=
Any
.
Pack
(
message
);
Assert
.
Throws
<
InvalidOperationException
>(()
=>
JsonFormatter
.
Default
.
Format
(
any
));
}
/// <summary>
/// Checks that the actual JSON is the same as the expected JSON - but after replacing
/// all apostrophes in the expected JSON with double quotes. This basically makes the tests easier
...
...
This diff is collapsed.
Click to expand it.
csharp/src/Google.Protobuf.Test/JsonParserTest.cs
View file @
e4af879b
...
...
@@ -30,6 +30,7 @@
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#endregion
using
Google.Protobuf.Reflection
;
using
Google.Protobuf.TestProtos
;
using
Google.Protobuf.WellKnownTypes
;
using
NUnit.Framework
;
...
...
@@ -717,6 +718,55 @@ namespace Google.Protobuf
CollectionAssert
.
AreEqual
(
expectedPaths
,
parsed
.
Paths
);
}
[
Test
]
public
void
Any_RegularMessage
()
{
var
registry
=
TypeRegistry
.
FromMessages
(
TestAllTypes
.
Descriptor
);
var
formatter
=
new
JsonFormatter
(
new
JsonFormatter
.
Settings
(
false
,
TypeRegistry
.
FromMessages
(
TestAllTypes
.
Descriptor
)));
var
message
=
new
TestAllTypes
{
SingleInt32
=
10
,
SingleNestedMessage
=
new
TestAllTypes
.
Types
.
NestedMessage
{
Bb
=
20
}
};
var
original
=
Any
.
Pack
(
message
);
var
json
=
formatter
.
Format
(
original
);
// This is tested in JsonFormatterTest
var
parser
=
new
JsonParser
(
new
JsonParser
.
Settings
(
10
,
registry
));
Assert
.
AreEqual
(
original
,
parser
.
Parse
<
Any
>(
json
));
string
valueFirstJson
=
"{ \"singleInt32\": 10, \"singleNestedMessage\": { \"bb\": 20 }, \"@type\": \"type.googleapis.com/protobuf_unittest.TestAllTypes\" }"
;
Assert
.
AreEqual
(
original
,
parser
.
Parse
<
Any
>(
valueFirstJson
));
}
[
Test
]
public
void
Any_UnknownType
()
{
string
json
=
"{ \"@type\": \"type.googleapis.com/bogus\" }"
;
Assert
.
Throws
<
InvalidOperationException
>(()
=>
Any
.
Parser
.
ParseJson
(
json
));
}
[
Test
]
public
void
Any_WellKnownType
()
{
var
registry
=
TypeRegistry
.
FromMessages
(
Timestamp
.
Descriptor
);
var
formatter
=
new
JsonFormatter
(
new
JsonFormatter
.
Settings
(
false
,
registry
));
var
timestamp
=
new
DateTime
(
1673
,
6
,
19
,
12
,
34
,
56
,
DateTimeKind
.
Utc
).
ToTimestamp
();
var
original
=
Any
.
Pack
(
timestamp
);
var
json
=
formatter
.
Format
(
original
);
// This is tested in JsonFormatterTest
var
parser
=
new
JsonParser
(
new
JsonParser
.
Settings
(
10
,
registry
));
Assert
.
AreEqual
(
original
,
parser
.
Parse
<
Any
>(
json
));
string
valueFirstJson
=
"{ \"value\": \"1673-06-19T12:34:56Z\", \"@type\": \"type.googleapis.com/google.protobuf.Timestamp\" }"
;
Assert
.
AreEqual
(
original
,
parser
.
Parse
<
Any
>(
valueFirstJson
));
}
[
Test
]
public
void
Any_Nested
()
{
var
registry
=
TypeRegistry
.
FromMessages
(
TestWellKnownTypes
.
Descriptor
,
TestAllTypes
.
Descriptor
);
var
formatter
=
new
JsonFormatter
(
new
JsonFormatter
.
Settings
(
false
,
registry
));
var
parser
=
new
JsonParser
(
new
JsonParser
.
Settings
(
10
,
registry
));
var
doubleNestedMessage
=
new
TestAllTypes
{
SingleInt32
=
20
};
var
nestedMessage
=
Any
.
Pack
(
doubleNestedMessage
);
var
message
=
new
TestWellKnownTypes
{
AnyField
=
Any
.
Pack
(
nestedMessage
)
};
var
json
=
formatter
.
Format
(
message
);
// Use the descriptor-based parser just for a change.
Assert
.
AreEqual
(
message
,
parser
.
Parse
(
json
,
TestWellKnownTypes
.
Descriptor
));
}
[
Test
]
public
void
DataAfterObject
()
{
...
...
This diff is collapsed.
Click to expand it.
csharp/src/Google.Protobuf.Test/JsonTokenizerTest.cs
View file @
e4af879b
...
...
@@ -85,7 +85,7 @@ namespace Google.Protobuf
public
void
ObjectDepth
()
{
string
json
=
"{ \"foo\": { \"x\": 1, \"y\": [ 0 ] } }"
;
var
tokenizer
=
new
JsonTokeniz
er
(
new
StringReader
(
json
));
var
tokenizer
=
JsonTokenizer
.
FromTextRead
er
(
new
StringReader
(
json
));
// If we had more tests like this, I'd introduce a helper method... but for one test, it's not worth it.
Assert
.
AreEqual
(
0
,
tokenizer
.
ObjectDepth
);
Assert
.
AreEqual
(
JsonToken
.
StartObject
,
tokenizer
.
Next
());
...
...
@@ -118,7 +118,7 @@ namespace Google.Protobuf
public
void
ObjectDepth_WithPushBack
()
{
string
json
=
"{}"
;
var
tokenizer
=
new
JsonTokeniz
er
(
new
StringReader
(
json
));
var
tokenizer
=
JsonTokenizer
.
FromTextRead
er
(
new
StringReader
(
json
));
Assert
.
AreEqual
(
0
,
tokenizer
.
ObjectDepth
);
var
token
=
tokenizer
.
Next
();
Assert
.
AreEqual
(
1
,
tokenizer
.
ObjectDepth
);
...
...
@@ -275,7 +275,7 @@ namespace Google.Protobuf
// Note: we don't test that the earlier tokens are exactly as expected,
// partly because that's hard to parameterize.
var
reader
=
new
StringReader
(
json
.
Replace
(
'\''
,
'"'
));
var
tokenizer
=
new
JsonTokeniz
er
(
reader
);
var
tokenizer
=
JsonTokenizer
.
FromTextRead
er
(
reader
);
for
(
int
i
=
0
;
i
<
expectedValidTokens
;
i
++)
{
Assert
.
IsNotNull
(
tokenizer
.
Next
());
...
...
@@ -334,7 +334,7 @@ namespace Google.Protobuf
[
Test
]
public
void
NextAfterEndDocumentThrows
()
{
var
tokenizer
=
new
JsonTokeniz
er
(
new
StringReader
(
"null"
));
var
tokenizer
=
JsonTokenizer
.
FromTextRead
er
(
new
StringReader
(
"null"
));
Assert
.
AreEqual
(
JsonToken
.
Null
,
tokenizer
.
Next
());
Assert
.
AreEqual
(
JsonToken
.
EndDocument
,
tokenizer
.
Next
());
Assert
.
Throws
<
InvalidOperationException
>(()
=>
tokenizer
.
Next
());
...
...
@@ -343,7 +343,7 @@ namespace Google.Protobuf
[
Test
]
public
void
CanPushBackEndDocument
()
{
var
tokenizer
=
new
JsonTokeniz
er
(
new
StringReader
(
"null"
));
var
tokenizer
=
JsonTokenizer
.
FromTextRead
er
(
new
StringReader
(
"null"
));
Assert
.
AreEqual
(
JsonToken
.
Null
,
tokenizer
.
Next
());
Assert
.
AreEqual
(
JsonToken
.
EndDocument
,
tokenizer
.
Next
());
tokenizer
.
PushBack
(
JsonToken
.
EndDocument
);
...
...
@@ -373,7 +373,7 @@ namespace Google.Protobuf
private
static
void
AssertTokensNoReplacement
(
string
json
,
params
JsonToken
[]
expectedTokens
)
{
var
reader
=
new
StringReader
(
json
);
var
tokenizer
=
new
JsonTokeniz
er
(
reader
);
var
tokenizer
=
JsonTokenizer
.
FromTextRead
er
(
reader
);
for
(
int
i
=
0
;
i
<
expectedTokens
.
Length
;
i
++)
{
var
actualToken
=
tokenizer
.
Next
();
...
...
@@ -393,7 +393,7 @@ namespace Google.Protobuf
private
static
void
AssertThrowsAfter
(
string
json
,
params
JsonToken
[]
expectedTokens
)
{
var
reader
=
new
StringReader
(
json
);
var
tokenizer
=
new
JsonTokeniz
er
(
reader
);
var
tokenizer
=
JsonTokenizer
.
FromTextRead
er
(
reader
);
for
(
int
i
=
0
;
i
<
expectedTokens
.
Length
;
i
++)
{
var
actualToken
=
tokenizer
.
Next
();
...
...
This diff is collapsed.
Click to expand it.
csharp/src/Google.Protobuf/JsonFormatter.cs
View file @
e4af879b
...
...
@@ -55,6 +55,12 @@ namespace Google.Protobuf
/// </remarks>
public
sealed
class
JsonFormatter
{
internal
const
string
AnyTypeUrlField
=
"@type"
;
internal
const
string
AnyWellKnownTypeValueField
=
"value"
;
private
const
string
TypeUrlPrefix
=
"type.googleapis.com"
;
private
const
string
NameValueSeparator
=
": "
;
private
const
string
PropertySeparator
=
", "
;
private
static
JsonFormatter
defaultInstance
=
new
JsonFormatter
(
Settings
.
Default
);
/// <summary>
...
...
@@ -130,7 +136,7 @@ namespace Google.Protobuf
/// <returns>The formatted message.</returns>
public
string
Format
(
IMessage
message
)
{
Preconditions
.
CheckNotNull
(
message
,
"message"
);
Preconditions
.
CheckNotNull
(
message
,
nameof
(
message
)
);
StringBuilder
builder
=
new
StringBuilder
();
if
(
message
.
Descriptor
.
IsWellKnownType
)
{
...
...
@@ -151,13 +157,18 @@ namespace Google.Protobuf
return
;
}
builder
.
Append
(
"{ "
);
bool
writtenFields
=
WriteMessageFields
(
builder
,
message
,
false
);
builder
.
Append
(
writtenFields
?
" }"
:
"}"
);
}
private
bool
WriteMessageFields
(
StringBuilder
builder
,
IMessage
message
,
bool
assumeFirstFieldWritten
)
{
var
fields
=
message
.
Descriptor
.
Fields
;
bool
first
=
true
;
bool
first
=
!
assumeFirstFieldWritten
;
// First non-oneof fields
foreach
(
var
field
in
fields
.
InFieldNumberOrder
())
{
var
accessor
=
field
.
Accessor
;
// Oneofs are written later
if
(
field
.
ContainingOneof
!=
null
&&
field
.
ContainingOneof
.
Accessor
.
GetCaseFieldDescriptor
(
message
)
!=
field
)
{
continue
;
...
...
@@ -178,14 +189,14 @@ namespace Google.Protobuf
// Okay, all tests complete: let's write the field value...
if
(!
first
)
{
builder
.
Append
(
", "
);
builder
.
Append
(
PropertySeparator
);
}
WriteString
(
builder
,
ToCamelCase
(
accessor
.
Descriptor
.
Name
));
builder
.
Append
(
": "
);
builder
.
Append
(
NameValueSeparator
);
WriteValue
(
builder
,
value
);
first
=
false
;
}
builder
.
Append
(
first
?
"}"
:
" }"
)
;
return
!
first
;
}
// Converted from src/google/protobuf/util/internal/utility.cc ToCamelCase
...
...
@@ -378,6 +389,8 @@ namespace Google.Protobuf
/// </summary>
private
void
WriteWellKnownTypeValue
(
StringBuilder
builder
,
MessageDescriptor
descriptor
,
object
value
,
bool
inField
)
{
// Currently, we can never actually get here, because null values are always handled by the caller. But if we *could*,
// this would do the right thing.
if
(
value
==
null
)
{
WriteNull
(
builder
);
...
...
@@ -429,6 +442,11 @@ namespace Google.Protobuf
WriteStructFieldValue
(
builder
,
(
IMessage
)
value
);
return
;
}
if
(
descriptor
.
FullName
==
Any
.
Descriptor
.
FullName
)
{
WriteAny
(
builder
,
(
IMessage
)
value
);
return
;
}
WriteMessage
(
builder
,
(
IMessage
)
value
);
}
...
...
@@ -496,6 +514,46 @@ namespace Google.Protobuf
AppendEscapedString
(
builder
,
string
.
Join
(
","
,
paths
.
Cast
<
string
>().
Select
(
ToCamelCase
)));
}
private
void
WriteAny
(
StringBuilder
builder
,
IMessage
value
)
{
string
typeUrl
=
(
string
)
value
.
Descriptor
.
Fields
[
Any
.
TypeUrlFieldNumber
].
Accessor
.
GetValue
(
value
);
ByteString
data
=
(
ByteString
)
value
.
Descriptor
.
Fields
[
Any
.
ValueFieldNumber
].
Accessor
.
GetValue
(
value
);
string
typeName
=
GetTypeName
(
typeUrl
);
MessageDescriptor
descriptor
=
settings
.
TypeRegistry
.
Find
(
typeName
);
if
(
descriptor
==
null
)
{
throw
new
InvalidOperationException
(
$"Type registry has no descriptor for type name '
{
typeName
}
'"
);
}
IMessage
message
=
descriptor
.
Parser
.
ParseFrom
(
data
);
builder
.
Append
(
"{ "
);
WriteString
(
builder
,
AnyTypeUrlField
);
builder
.
Append
(
NameValueSeparator
);
WriteString
(
builder
,
typeUrl
);
if
(
descriptor
.
IsWellKnownType
)
{
builder
.
Append
(
PropertySeparator
);
WriteString
(
builder
,
AnyWellKnownTypeValueField
);
builder
.
Append
(
NameValueSeparator
);
WriteWellKnownTypeValue
(
builder
,
descriptor
,
message
,
true
);
}
else
{
WriteMessageFields
(
builder
,
message
,
true
);
}
builder
.
Append
(
" }"
);
}
internal
static
string
GetTypeName
(
String
typeUrl
)
{
string
[]
parts
=
typeUrl
.
Split
(
'/'
);
if
(
parts
.
Length
!=
2
||
parts
[
0
]
!=
TypeUrlPrefix
)
{
throw
new
InvalidProtocolBufferException
(
$"Invalid type url:
{
typeUrl
}
"
);
}
return
parts
[
1
];
}
/// <summary>
/// Appends a number of nanoseconds to a StringBuilder. Either 0 digits are added (in which
/// case no "." is appended), or 3 6 or 9 digits.
...
...
@@ -537,10 +595,10 @@ namespace Google.Protobuf
if
(!
first
)
{
builder
.
Append
(
", "
);
builder
.
Append
(
PropertySeparator
);
}
WriteString
(
builder
,
key
);
builder
.
Append
(
": "
);
builder
.
Append
(
NameValueSeparator
);
WriteStructFieldValue
(
builder
,
value
);
first
=
false
;
}
...
...
@@ -590,7 +648,7 @@ namespace Google.Protobuf
}
if
(!
first
)
{
builder
.
Append
(
", "
);
builder
.
Append
(
PropertySeparator
);
}
WriteValue
(
builder
,
value
);
first
=
false
;
...
...
@@ -611,7 +669,7 @@ namespace Google.Protobuf
}
if
(!
first
)
{
builder
.
Append
(
", "
);
builder
.
Append
(
PropertySeparator
);
}
string
keyText
;
if
(
pair
.
Key
is
string
)
...
...
@@ -635,7 +693,7 @@ namespace Google.Protobuf
throw
new
ArgumentException
(
"Unhandled dictionary key type: "
+
pair
.
Key
.
GetType
());
}
WriteString
(
builder
,
keyText
);
builder
.
Append
(
": "
);
builder
.
Append
(
NameValueSeparator
);
WriteValue
(
builder
,
pair
.
Value
);
first
=
false
;
}
...
...
@@ -750,28 +808,50 @@ namespace Google.Protobuf
/// </summary>
public
sealed
class
Settings
{
private
static
readonly
Settings
defaultInstance
=
new
Settings
(
false
);
/// <summary>
/// Default settings, as used by <see cref="JsonFormatter.Default"/>
/// </summary>
public
static
Settings
Default
{
get
{
return
defaultInstance
;
}
}
public
static
Settings
Default
{
get
;
}
private
readonly
bool
formatDefaultValues
;
// Workaround for the Mono compiler complaining about XML comments not being on
// valid language elements.
static
Settings
()
{
Default
=
new
Settings
(
false
);
}
/// <summary>
/// Whether fields whose values are the default for the field type (e.g. 0 for integers)
/// should be formatted (true) or omitted (false).
/// </summary>
public
bool
FormatDefaultValues
{
get
{
return
formatDefaultValues
;
}
}
public
bool
FormatDefaultValues
{
get
;
}
/// <summary>
/// The type registry used to format <see cref="Any"/> messages.
/// </summary>
public
TypeRegistry
TypeRegistry
{
get
;
}
// TODO: Work out how we're going to scale this to multiple settings. "WithXyz" methods?
/// <summary>
/// Creates a new <see cref="Settings"/> object with the specified formatting of default values
/// and an empty type registry.
/// </summary>
/// <param name="formatDefaultValues"><c>true</c> if default values (0, empty strings etc) should be formatted; <c>false</c> otherwise.</param>
public
Settings
(
bool
formatDefaultValues
)
:
this
(
formatDefaultValues
,
TypeRegistry
.
Empty
)
{
}
/// <summary>
/// Creates a new <see cref="Settings"/> object with the specified formatting of default values.
/// Creates a new <see cref="Settings"/> object with the specified formatting of default values
/// and type registry.
/// </summary>
/// <param name="formatDefaultValues"><c>true</c> if default values (0, empty strings etc) should be formatted; <c>false</c> otherwise.</param>
public
Settings
(
bool
formatDefaultValues
)
/// <param name="typeRegistry">The <see cref="TypeRegistry"/> to use when formatting <see cref="Any"/> messages.</param>
public
Settings
(
bool
formatDefaultValues
,
TypeRegistry
typeRegistry
)
{
this
.
formatDefaultValues
=
formatDefaultValues
;
FormatDefaultValues
=
formatDefaultValues
;
TypeRegistry
=
Preconditions
.
CheckNotNull
(
typeRegistry
,
nameof
(
typeRegistry
));
}
}
}
...
...
This diff is collapsed.
Click to expand it.
csharp/src/Google.Protobuf/JsonParser.cs
View file @
e4af879b
This diff is collapsed.
Click to expand it.
csharp/src/Google.Protobuf/JsonTokenizer.cs
View file @
e4af879b
This diff is collapsed.
Click to expand it.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment