1 module google.protobuf.field_mask;
2 
3 import std.json : JSONValue;
4 import google.protobuf;
5 
6 struct FieldMask
7 {
8     @Proto(1) string[] paths = protoDefaultValue!(string[]);
9 
10     JSONValue toJSONValue()()
11     {
12         import std.algorithm : map;
13         import std.array : join;
14         import google.protobuf.json_encoding : toJSONValue;
15 
16         return paths.map!(a => a.toCamelCase).join(",").toJSONValue;
17     }
18 
19     FieldMask fromJSONValue()(JSONValue value)
20     {
21         import std.algorithm : map, splitter;
22         import std.array : array;
23         import std.exception : enforce;
24         import std.json : JSONType;
25 
26         if (value.type == JSONType.null_)
27         {
28             paths = protoDefaultValue!(string[]);
29             return this;
30         }
31 
32         enforce!ProtobufException(value.type == JSONType..string, "FieldMask JSON encoding must be a string");
33 
34         paths = value.str.splitter(",").map!(a => a.toSnakeCase).array;
35 
36         return this;
37     }
38 }
39 
40 unittest
41 {
42     assert(FieldMask([]).toJSONValue == JSONValue(""));
43     assert(FieldMask(["foo"]).toJSONValue == JSONValue("foo"));
44     assert(FieldMask(["foo", "bar_baz"]).toJSONValue == JSONValue("foo,barBaz"));
45     assert(FieldMask(["foo", "bar_baz.qux"]).toJSONValue == JSONValue("foo,barBaz.qux"));
46 }
47 
48 unittest
49 {
50     FieldMask foo;
51     assert(FieldMask([]) == foo.fromJSONValue(JSONValue(null)));
52     assert(FieldMask([]) == foo.fromJSONValue(JSONValue("")));
53     assert(FieldMask(["foo"]) == foo.fromJSONValue(JSONValue("foo")));
54     assert(FieldMask(["foo", "bar_baz"]) == foo.fromJSONValue(JSONValue("foo,barBaz")));
55     assert(FieldMask(["foo", "bar_baz.qux"]) == foo.fromJSONValue(JSONValue("foo,barBaz.qux")));
56 }
57 
58 string toCamelCase(string snakeCase) pure
59 {
60     import std.array : Appender;
61     import std.ascii : isLower, isUpper, toUpper;
62     import std.exception : enforce;
63 
64     Appender!string result;
65     bool capitalizeNext;
66     bool wordStart = true;
67     foreach (c; snakeCase)
68     {
69         enforce!ProtobufException(!c.isUpper, "Invalid field mask " ~ snakeCase);
70         enforce!ProtobufException(!wordStart || c.isLower || c == '_' || c == '.', "Invalid field mask " ~ snakeCase);
71         wordStart = (c == '_' || c == '.');
72         if (c == '_')
73         {
74             enforce!ProtobufException(!capitalizeNext, "Invalid field mask " ~ snakeCase);
75             capitalizeNext = true;
76             continue;
77         }
78         if (capitalizeNext)
79         {
80             result ~= c.toUpper;
81             capitalizeNext = false;
82             continue;
83         }
84         result ~= c;
85     }
86 
87     return result.data;
88 }
89 
90 unittest
91 {
92     import std.exception : assertThrown;
93 
94     assert("".toCamelCase == "");
95     assert("foo".toCamelCase == "foo");
96     assert("foo1".toCamelCase == "foo1");
97     assert("foo_bar".toCamelCase == "fooBar");
98     assert("_foo_bar".toCamelCase == "FooBar");
99     assert("foo_bar_baz_qux".toCamelCase == "fooBarBazQux");
100     assertThrown!ProtobufException("__".toCamelCase);
101     assertThrown!ProtobufException("foo__bar".toCamelCase);
102     assertThrown!ProtobufException("fooBar".toCamelCase);
103     assertThrown!ProtobufException("foo_1".toCamelCase);
104     assertThrown!ProtobufException("1_foo".toCamelCase);
105     assertThrown!ProtobufException("_1_foo".toCamelCase);
106     
107     assert("foo.bar".toCamelCase == "foo.bar");
108     assert(".foo..bar.".toCamelCase == ".foo..bar.");
109     assert("foo_bar.baz_qux".toCamelCase == "fooBar.bazQux");
110 }
111 
112 string toSnakeCase(string camelCase) pure
113 {
114     import std.array : Appender;
115     import std.ascii : isUpper, toLower;
116     import std.exception : enforce;
117 
118     Appender!string result;
119 
120     foreach (c; camelCase)
121     {
122         enforce!ProtobufException(c != '_', "Invalid field mask " ~ camelCase);
123         if (c.isUpper)
124         {
125             result ~= '_';
126             result ~= c.toLower;
127         }
128         else
129         {
130             result ~= c;
131         }
132     }
133 
134     return result.data;
135 }
136 
137 unittest
138 {
139     import std.exception : assertThrown;
140 
141     assert("".toSnakeCase == "");
142     assert("fooBar".toSnakeCase == "foo_bar");
143     assert("foo1".toSnakeCase == "foo1");
144     assert("FooBar".toSnakeCase == "_foo_bar");
145     assert("fooBarBazQux".toSnakeCase == "foo_bar_baz_qux");
146     assertThrown!ProtobufException("foo_Bar".toSnakeCase);
147     assertThrown!ProtobufException("foo_Bar".toSnakeCase);
148     
149     assert("foo.bar".toSnakeCase == "foo.bar");
150     assert(".foo..bar.".toSnakeCase == ".foo..bar.");
151     assert("fooBar.bazQux".toSnakeCase == "foo_bar.baz_qux");
152 }