diff --git a/CHANGELOG.md b/CHANGELOG.md index c781e08..80fda17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/text/json/Decode.d b/src/text/json/Decode.d index e3e8df8..8283cde 100644 --- a/src/text/json/Decode.d +++ b/src/text/json/Decode.d @@ -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; @@ -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"( diff --git a/src/text/json/Enum.d b/src/text/json/Enum.d index 0edfb80..b875640 100644 --- a/src/text/json/Enum.d +++ b/src/text/json/Enum.d @@ -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) @@ -88,7 +113,10 @@ private string screamingSnake(string text) void flush() { - split ~= buffer; + if (!buffer.empty) + { + split ~= buffer; + } buffer = null; } foreach (ch; text) @@ -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; diff --git a/unittest/text/json/DecodeTest.d b/unittest/text/json/DecodeTest.d index ef9acb3..213562f 100644 --- a/unittest/text/json/DecodeTest.d +++ b/unittest/text/json/DecodeTest.d @@ -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