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
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
298 additions
and
58 deletions
+298
-58
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
+100
-32
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
...
...
@@ -47,32 +47,38 @@ namespace Google.Protobuf
/// between values. It validates the token stream as it goes - so callers can assume that the
/// tokens it produces are appropriate. For example, it would never produce "start object, end array."
/// </para>
/// <para>Implementation details: the base class handles single token push-back and </para>
/// <para>Not thread-safe.</para>
/// </remarks>
internal
sealed
class
JsonTokenizer
internal
abstract
class
JsonTokenizer
{
// The set of states in which a value is valid next token.
private
static
readonly
State
ValueStates
=
State
.
ArrayStart
|
State
.
ArrayAfterComma
|
State
.
ObjectAfterColon
|
State
.
StartOfDocument
;
private
readonly
Stack
<
ContainerType
>
containerStack
=
new
Stack
<
ContainerType
>();
private
readonly
PushBackReader
reader
;
private
JsonToken
bufferedToken
;
private
State
state
;
private
int
objectDepth
=
0
;
/// <summary>
/// Returns the depth of the stack, purely in objects (not collections).
/// Informally, this is the number of remaining unclosed '{' characters we have.
/// Creates a tokenizer that reads from the given text reader.
/// </summary>
internal
int
ObjectDepth
{
get
{
return
objectDepth
;
}
}
internal
static
JsonTokenizer
FromTextReader
(
TextReader
reader
)
{
return
new
JsonTextTokenizer
(
reader
);
}
internal
JsonTokenizer
(
TextReader
reader
)
/// <summary>
/// Creates a tokenizer that first replays the given list of tokens, then continues reading
/// from another tokenizer. Note that if the returned tokenizer is "pushed back", that does not push back
/// on the continuation tokenizer, or vice versa. Care should be taken when using this method - it was
/// created for the sake of Any parsing.
/// </summary>
internal
static
JsonTokenizer
FromReplayedTokens
(
IList
<
JsonToken
>
tokens
,
JsonTokenizer
continuation
)
{
this
.
reader
=
new
PushBackReader
(
reader
);
state
=
State
.
StartOfDocument
;
containerStack
.
Push
(
ContainerType
.
Document
);
return
new
JsonReplayTokenizer
(
tokens
,
continuation
);
}
/// <summary>
/// Returns the depth of the stack, purely in objects (not collections).
/// Informally, this is the number of remaining unclosed '{' characters we have.
/// </summary>
internal
int
ObjectDepth
{
get
;
private
set
;
}
// TODO: Why do we allow a different token to be pushed back? It might be better to always remember the previous
// token returned, and allow a parameterless Rewind() method (which could only be called once, just like the current PushBack).
internal
void
PushBack
(
JsonToken
token
)
...
...
@@ -84,11 +90,11 @@ namespace Google.Protobuf
bufferedToken
=
token
;
if
(
token
.
Type
==
JsonToken
.
TokenType
.
StartObject
)
{
o
bjectDepth
--;
O
bjectDepth
--;
}
else
if
(
token
.
Type
==
JsonToken
.
TokenType
.
EndObject
)
{
o
bjectDepth
++;
O
bjectDepth
++;
}
}
...
...
@@ -96,32 +102,95 @@ namespace Google.Protobuf
/// Returns the next JSON token in the stream. An EndDocument token is returned to indicate the end of the stream,
/// after which point <c>Next()</c> should not be called again.
/// </summary>
/// <remarks>
/// This method essentially just loops through characters skipping whitespace, validating and
/// changing state (e.g. from ObjectBeforeColon to ObjectAfterColon)
/// until it reaches something which will be a genuine token (e.g. a start object, or a value) at which point
/// it returns the token. Although the method is large, it would be relatively hard to break down further... most
/// of it is the large switch statement, which sometimes returns and sometimes doesn't.
/// </remarks>
/// <remarks>This implementation provides single-token buffering, and calls <see cref="NextImpl"/> if there is no buffered token.</remarks>
/// <returns>The next token in the stream. This is never null.</returns>
/// <exception cref="InvalidOperationException">This method is called after an EndDocument token has been returned</exception>
/// <exception cref="InvalidJsonException">The input text does not comply with RFC 7159</exception>
internal
JsonToken
Next
()
{
JsonToken
tokenToReturn
;
if
(
bufferedToken
!=
null
)
{
var
ret
=
bufferedToken
;
tokenToReturn
=
bufferedToken
;
bufferedToken
=
null
;
if
(
ret
.
Type
==
JsonToken
.
TokenType
.
StartObject
)
}
else
{
objectDepth
++
;
tokenToReturn
=
NextImpl
()
;
}
else
if
(
ret
.
Type
==
JsonToken
.
TokenType
.
End
Object
)
if
(
tokenToReturn
.
Type
==
JsonToken
.
TokenType
.
Start
Object
)
{
objectDepth
--
;
ObjectDepth
++
;
}
return
ret
;
else
if
(
tokenToReturn
.
Type
==
JsonToken
.
TokenType
.
EndObject
)
{
ObjectDepth
--;
}
return
tokenToReturn
;
}
/// <summary>
/// Returns the next JSON token in the stream, when requested by the base class. (The <see cref="Next"/> method delegates
/// to this if it doesn't have a buffered token.)
/// </summary>
/// <exception cref="InvalidOperationException">This method is called after an EndDocument token has been returned</exception>
/// <exception cref="InvalidJsonException">The input text does not comply with RFC 7159</exception>
protected
abstract
JsonToken
NextImpl
();
/// <summary>
/// Tokenizer which first exhausts a list of tokens, then consults another tokenizer.
/// </summary>
private
class
JsonReplayTokenizer
:
JsonTokenizer
{
private
readonly
IList
<
JsonToken
>
tokens
;
private
readonly
JsonTokenizer
nextTokenizer
;
private
int
nextTokenIndex
;
internal
JsonReplayTokenizer
(
IList
<
JsonToken
>
tokens
,
JsonTokenizer
nextTokenizer
)
{
this
.
tokens
=
tokens
;
this
.
nextTokenizer
=
nextTokenizer
;
}
// FIXME: Object depth not maintained...
protected
override
JsonToken
NextImpl
()
{
if
(
nextTokenIndex
>=
tokens
.
Count
)
{
return
nextTokenizer
.
Next
();
}
return
tokens
[
nextTokenIndex
++];
}
}
/// <summary>
/// Tokenizer which does all the *real* work of parsing JSON.
/// </summary>
private
sealed
class
JsonTextTokenizer
:
JsonTokenizer
{
// The set of states in which a value is valid next token.
private
static
readonly
State
ValueStates
=
State
.
ArrayStart
|
State
.
ArrayAfterComma
|
State
.
ObjectAfterColon
|
State
.
StartOfDocument
;
private
readonly
Stack
<
ContainerType
>
containerStack
=
new
Stack
<
ContainerType
>();
private
readonly
PushBackReader
reader
;
private
State
state
;
internal
JsonTextTokenizer
(
TextReader
reader
)
{
this
.
reader
=
new
PushBackReader
(
reader
);
state
=
State
.
StartOfDocument
;
containerStack
.
Push
(
ContainerType
.
Document
);
}
/// <remarks>
/// This method essentially just loops through characters skipping whitespace, validating and
/// changing state (e.g. from ObjectBeforeColon to ObjectAfterColon)
/// until it reaches something which will be a genuine token (e.g. a start object, or a value) at which point
/// it returns the token. Although the method is large, it would be relatively hard to break down further... most
/// of it is the large switch statement, which sometimes returns and sometimes doesn't.
/// </remarks>
protected
override
JsonToken
NextImpl
()
{
if
(
state
==
State
.
ReaderExhausted
)
{
throw
new
InvalidOperationException
(
"Next() called after end of document"
);
...
...
@@ -167,12 +236,10 @@ namespace Google.Protobuf
ValidateState
(
ValueStates
,
"Invalid state to read an open brace: "
);
state
=
State
.
ObjectStart
;
containerStack
.
Push
(
ContainerType
.
Object
);
objectDepth
++;
return
JsonToken
.
StartObject
;
case
'}'
:
ValidateState
(
State
.
ObjectAfterProperty
|
State
.
ObjectStart
,
"Invalid state to read a close brace: "
);
PopContainer
();
objectDepth
--;
return
JsonToken
.
EndObject
;
case
'['
:
ValidateState
(
ValueStates
,
"Invalid state to read an open square bracket: "
);
...
...
@@ -667,4 +734,5 @@ namespace Google.Protobuf
}
}
}
}
}
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