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
Dec 02, 2015
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
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
...
...
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
()
{
...
...
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
();
...
...
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
));
}
}
}
...
...
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