1 module text.json.Enum;
2 
3 import std.algorithm;
4 import std.ascii : isLower;
5 import std.json;
6 import std.range;
7 import std.utf;
8 
9 version (unittest) import dshould;
10 
11 /**
12  * Helper to encode a DStyle enum ("entryName") as JSON style ("ENTRY_NAME").
13  *
14  * Use like so: `alias encode = encodeEnum!EnumType;` when forming your encode overload.
15  */
16 string encodeEnum(T)(const T value)
17 if (is(T == enum))
18 {
19     import std.conv : to;
20     import std.uni : toUpper;
21 
22     const enumString = value.to!string;
23 
24     return enumString.splitByPredicate!isWord.map!toUpper.join("_");
25 }
26 
27 /// ditto
28 unittest
29 {
30     import text.json.Encode : encodeJson;
31 
32     enum Enum
33     {
34         testValue,
35         isHttp,
36     }
37 
38     alias encode = encodeEnum!Enum;
39 
40     encodeJson!(Enum, encode)(Enum.testValue).should.be(JSONValue("TEST_VALUE"));
41     encodeJson!(Enum, encode)(Enum.isHttp).should.be(JSONValue("IS_HTTP"));
42 }
43 
44 /**
45  * Helper to decode a JSON style enum string (ENTRY_NAME) as a DStyle enum (entryName).
46  *
47  * Use like so: `alias decode = decodeEnum!EnumType;` when forming your encode overload.
48  * Throws: JSONException if the input text does not represent an enum member.
49  */
50 template decodeEnum(T)
51 if (is(T == enum))
52 {
53     U decodeEnum(U : T)(const string text)
54     {
55         import std.range : only;
56         import std.conv : ConvException, to;
57         import std.exception : enforce;
58         import std.format : format;
59         import std..string : capitalize;
60         import std.uni : toLower;
61 
62         enforce!JSONException(!text.empty, "expected member of " ~ T.stringof);
63 
64         auto split = text.splitter("_");
65         const camelCase = chain(split.front.toLower.only, split.dropOne.map!capitalize).join;
66 
67         try
68         {
69             return camelCase.to!T;
70         }
71         catch (ConvException convException)
72         {
73             throw new JSONException(
74                 format!"expected member of %s, not %s (or in D, '%s')"(T.stringof, text, camelCase));
75         }
76     }
77 }
78 
79 /// ditto
80 unittest
81 {
82     import text.json.Decode : decodeJson;
83 
84     enum Enum
85     {
86         testValue,
87         isHttp,
88     }
89 
90     alias decode = decodeEnum!Enum;
91 
92     decodeJson!(Enum, decode)(JSONValue("TEST_VALUE")).should.be(Enum.testValue);
93     decodeJson!(Enum, decode)(JSONValue("IS_HTTP")).should.be(Enum.isHttp);
94     decodeJson!(Enum, decode)(JSONValue("")).should.throwA!JSONException;
95     decodeJson!(Enum, decode)(JSONValue("ISNT_HTTP")).should.throwA!JSONException(
96         "expected member of Enum, not ISNT_HTTP (or in D, 'isntHttp')");
97 }
98 
99 alias isWord = text => text.length > 0 && text.drop(1).all!isLower;
100 
101 private string[] splitByPredicate(alias pred)(string text)
102 {
103     string[] result;
104     while (text.length > 0)
105     {
106         size_t scan = 0;
107 
108         while (scan < text.length)
109         {
110             const newscan = scan + text[scan .. $].stride;
111 
112             if (pred(text[0 .. newscan]))
113             {
114                 scan = newscan;
115             }
116             else
117             {
118                 break;
119             }
120         }
121 
122         result ~= text[0 .. scan];
123         text = text[scan .. $];
124     }
125     return result;
126 }
127 
128 unittest
129 {
130     splitByPredicate!isWord("FooBar").should.be(["Foo", "Bar"]);
131     splitByPredicate!isWord("FooBAR").should.be(["Foo", "B", "A", "R"]);
132     splitByPredicate!isWord("").should.be([]);
133 }