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 : JSON_TYPE;
25 
26         if (value.type == JSON_TYPE.NULL)
27         {
28             paths = protoDefaultValue!(string[]);
29             return this;
30         }
31 
32         enforce!ProtobufException(value.type == JSON_TYPE.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(["foo"]).toJSONValue == JSONValue("foo"));
43     assert(FieldMask(["foo", "bar_baz"]).toJSONValue == JSONValue("foo,barBaz"));
44     assert(FieldMask(["foo", "bar_baz.qux"]).toJSONValue == JSONValue("foo,barBaz.qux"));
45 }
46 
47 unittest
48 {
49     FieldMask foo;
50     assert(FieldMask(["foo"]) == foo.fromJSONValue(JSONValue("foo")));
51     assert(FieldMask(["foo", "bar_baz"]) == foo.fromJSONValue(JSONValue("foo,barBaz")));
52     assert(FieldMask(["foo", "bar_baz.qux"]) == foo.fromJSONValue(JSONValue("foo,barBaz.qux")));
53 }
54 
55 string toCamelCase(string snakeCase) pure
56 {
57     import std.array : Appender;
58     import std.ascii : isLower, isUpper, toUpper;
59     import std.exception : enforce;
60 
61     Appender!string result;
62     bool capitalizeNext;
63     bool wordStart = true;
64     foreach (c; snakeCase)
65     {
66         enforce!ProtobufException(!c.isUpper, "Invalid field mask " ~ snakeCase);
67         enforce!ProtobufException(!wordStart || c.isLower || c == '_' || c == '.', "Invalid field mask " ~ snakeCase);
68         wordStart = (c == '_' || c == '.');
69         if (c == '_')
70         {
71             enforce!ProtobufException(!capitalizeNext, "Invalid field mask " ~ snakeCase);
72             capitalizeNext = true;
73             continue;
74         }
75         if (capitalizeNext)
76         {
77             result ~= c.toUpper;
78             capitalizeNext = false;
79             continue;
80         }
81         result ~= c;
82     }
83 
84     return result.data;
85 }
86 
87 unittest
88 {
89     import std.exception : assertThrown;
90 
91     assert("foo".toCamelCase == "foo");
92     assert("foo1".toCamelCase == "foo1");
93     assert("foo_bar".toCamelCase == "fooBar");
94     assert("_foo_bar".toCamelCase == "FooBar");
95     assert("foo_bar_baz_qux".toCamelCase == "fooBarBazQux");
96     assertThrown!ProtobufException("__".toCamelCase);
97     assertThrown!ProtobufException("foo__bar".toCamelCase);
98     assertThrown!ProtobufException("fooBar".toCamelCase);
99     assertThrown!ProtobufException("foo_1".toCamelCase);
100     assertThrown!ProtobufException("1_foo".toCamelCase);
101     assertThrown!ProtobufException("_1_foo".toCamelCase);
102     
103     assert("foo.bar".toCamelCase == "foo.bar");
104     assert(".foo..bar.".toCamelCase == ".foo..bar.");
105     assert("foo_bar.baz_qux".toCamelCase == "fooBar.bazQux");
106 }
107 
108 string toSnakeCase(string camelCase) pure
109 {
110     import std.array : Appender;
111     import std.ascii : isUpper, toLower;
112     import std.exception : enforce;
113 
114     Appender!string result;
115 
116     foreach (c; camelCase)
117     {
118         enforce!ProtobufException(c != '_', "Invalid field mask " ~ camelCase);
119         if (c.isUpper)
120         {
121             result ~= '_';
122             result ~= c.toLower;
123         }
124         else
125         {
126             result ~= c;
127         }
128     }
129 
130     return result.data;
131 }
132 
133 unittest
134 {
135     import std.exception : assertThrown;
136 
137     assert("".toSnakeCase == "");
138     assert("fooBar".toSnakeCase == "foo_bar");
139     assert("foo1".toSnakeCase == "foo1");
140     assert("FooBar".toSnakeCase == "_foo_bar");
141     assert("fooBarBazQux".toSnakeCase == "foo_bar_baz_qux");
142     assertThrown!ProtobufException("foo_Bar".toSnakeCase);
143     assertThrown!ProtobufException("foo_Bar".toSnakeCase);
144     
145     assert("foo.bar".toSnakeCase == "foo.bar");
146     assert(".foo..bar.".toSnakeCase == ".foo..bar.");
147     assert("fooBar.bazQux".toSnakeCase == "foo_bar.baz_qux");
148 }