Skip to content

Commit

Permalink
When parsing enums, support both enum member and screaming-snake case.
Browse files Browse the repository at this point in the history
  • Loading branch information
FeepingCreature committed Feb 8, 2023
1 parent 5809337 commit 31f1be3
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 27 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.9.1] - 2023-02-08
### Fixed
- When decoding enums, accept both enum member and screaming-snake case.
This makes it easier to transition between them.
When `-version=strict` is set, only the default format is accepted.

## [1.9.0] - 2023-01-11
### Added
- Interpret boilerplate's `@AliasThis` equivalent to `alias this` in JSON encoding/decoding.
Expand Down
13 changes: 3 additions & 10 deletions src/text/json/Decode.d
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,8 @@ if (is(T == enum))
{
private T decodeValue(JsonStream)(ref JsonStream jsonStream, lazy string target)
{
import text.json.Enum : genericDecodeEnum;

scope(success)
{
jsonStream.popFront;
Expand All @@ -462,16 +464,7 @@ if (is(T == enum))
{
string str = jsonStream.front.literal.string;

try
{
return parse!(Unqual!T)(str);
}
catch (ConvException exception)
{
throw new JSONException(
format!"Invalid JSON:%s expected member of %s, but got \"%s\""
(target ? (" " ~ target) : null, T.stringof, str));
}
return genericDecodeEnum!(T, No.expectScreaming)(str, target);
}
throw new JSONException(
format!"Invalid JSON:%s expected enum string, but got %s"(
Expand Down
60 changes: 44 additions & 16 deletions src/text/json/Enum.d
Original file line number Diff line number Diff line change
Expand Up @@ -52,29 +52,54 @@ if (is(T == enum))
{
U decodeEnum(U : T)(const string text)
{
import std.conv : to;
import std.exception : enforce;
import std.format : format;
import std.traits : EnumMembers;
return genericDecodeEnum!(U, Yes.expectScreaming)(text);
}
}

package T genericDecodeEnum(T, Flag!"expectScreaming" expectScreaming)(const string text, const string target = null)
{
import std.conv : to;
import std.exception : enforce;
import std.format : format;
import std.traits : EnumMembers;

enforce!JSONException(!text.empty, "expected member of " ~ T.stringof);
version (strict)
{
enum bool checkCamelCase = expectScreaming == false;
enum bool checkScreamingCase = expectScreaming == true;
}
else
{
enum bool checkCamelCase = true;
enum bool checkScreamingCase = true;
}

switch (text)
switch (text)
{
static foreach (member; EnumMembers!T)
{
static foreach (member; EnumMembers!T)
static if (checkCamelCase)
{
case member.to!string.screamingSnake:
case member.to!string:
return member;
}
static if (checkScreamingCase && member.to!string.screamingSnake != member.to!string)
{
case member.to!string.screamingSnake:
return member;
}
default:
break;
}
default:
break;
}

alias allScreamingSnakes = () => [EnumMembers!T].map!(a => a.to!string.screamingSnake);
enum allMembers = [EnumMembers!T]
.map!(a => (checkScreamingCase ? [a.to!string.screamingSnake] : []) ~ (checkCamelCase ? [a.to!string] : []))
.join
.uniq;

throw new JSONException(
format!"expected member of %s (%-(%s, %)), not %s"(T.stringof, allScreamingSnakes(), text));
}
throw new JSONException(format!"Invalid JSON:%s expected member of %s (%-(%s, %)), but got \"%s\""(
(target ? (" " ~ target) : ""), T.stringof, allMembers, text));
}

private string screamingSnake(string text)
Expand All @@ -88,7 +113,10 @@ private string screamingSnake(string text)

void flush()
{
split ~= buffer;
if (!buffer.empty)
{
split ~= buffer;
}
buffer = null;
}
foreach (ch; text)
Expand Down Expand Up @@ -122,7 +150,7 @@ unittest
decodeJson!(Enum, decode)(JSONValue("IS_HTTP")).should.be(Enum.isHttp);
decodeJson!(Enum, decode)(JSONValue("")).should.throwA!JSONException;
decodeJson!(Enum, decode)(JSONValue("ISNT_HTTP")).should.throwA!JSONException(
"expected member of Enum (TEST_VALUE, IS_HTTP), not ISNT_HTTP");
`Invalid JSON: expected member of Enum (TEST_VALUE, testValue, IS_HTTP, isHttp), but got "ISNT_HTTP"`);
}

alias isWord = text => text.length > 0 && text.drop(1).all!isLower;
Expand Down
2 changes: 1 addition & 1 deletion unittest/text/json/DecodeTest.d
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ unittest
decode!OptionalValues(`{ "intValue": "" }`).should.throwA!JSONException
(`Invalid JSON: text.json.DecodeTest.OptionalValues.intValue expected int, but got ""`);
decode!OptionalValues(`{ "enumValue": "B" }`).should.throwA!JSONException
(`Invalid JSON: text.json.DecodeTest.OptionalValues.enumValue expected member of Enum, but got "B"`);
(`Invalid JSON: text.json.DecodeTest.OptionalValues.enumValue expected member of Enum (A), but got "B"`);
decode!OptionalValues(`{ "enumValue": 5 }`).should.throwA!JSONException
(`Invalid JSON: text.json.DecodeTest.OptionalValues.enumValue expected enum string, but got 5`);
decode!OptionalValues(`{ "stringValue": 5 }`).should.throwA!JSONException
Expand Down

0 comments on commit 31f1be3

Please sign in to comment.