diff --git a/README.md b/README.md index 082ff1b..16d6060 100644 --- a/README.md +++ b/README.md @@ -541,3 +541,26 @@ For example, imagine a `phoneNumber` scalar like this : .build() ``` + +## HexColorCode Scalar + + + + + + + + + +
Scalar DefinitionDescription
+scalar HexColorCode
+  @specifiedBy(url: 
+    "https://tools.ietf.org/html/bcp47"
+  )
+
The HexColorCode scalar handles hex color code. You can handle it via java.awt.Color (which was built into JDK)
+It Support Following Formats: + +- #RRGGBB +- #RGB +- #RRGGBBAA +- #RGBA diff --git a/src/main/java/graphql/scalars/ExtendedScalars.java b/src/main/java/graphql/scalars/ExtendedScalars.java index ce36232..0970206 100644 --- a/src/main/java/graphql/scalars/ExtendedScalars.java +++ b/src/main/java/graphql/scalars/ExtendedScalars.java @@ -2,6 +2,7 @@ import graphql.PublicApi; import graphql.scalars.alias.AliasedScalar; +import graphql.scalars.color.hex.HexColorCodeScalar; import graphql.scalars.country.code.CountryCodeScalar; import graphql.scalars.currency.CurrencyScalar; import graphql.scalars.datetime.DateScalar; @@ -150,6 +151,12 @@ public class ExtendedScalars { */ public static final GraphQLScalarType CountryCode = CountryCodeScalar.INSTANCE; + /** + * A field whose value is a hex color code + * See the Web colors for more details. + */ + public static final GraphQLScalarType HexColorCode = HexColorCodeScalar.INSTANCE; + /** * A UUID scalar that accepts a universally unique identifier and produces {@link * java.util.UUID} objects at runtime. diff --git a/src/main/java/graphql/scalars/color/hex/HexColorCodeScalar.java b/src/main/java/graphql/scalars/color/hex/HexColorCodeScalar.java new file mode 100644 index 0000000..0cdc721 --- /dev/null +++ b/src/main/java/graphql/scalars/color/hex/HexColorCodeScalar.java @@ -0,0 +1,120 @@ +package graphql.scalars.color.hex; + +import graphql.language.StringValue; +import graphql.language.Value; +import graphql.schema.*; + +import java.awt.*; +import java.util.function.Function; +import java.util.regex.Pattern; + +import static graphql.scalars.util.Kit.typeName; +/** + * Access this via {@link graphql.scalars.ExtendedScalars#HexColorCode} + * See the Web colors for more details. + * @implNote Supports the following formats: #RGB, #RGBA, #RRGGBB, #RRGGBBAA. Need to be prefixed with '#' + */ +public class HexColorCodeScalar { + + public static final GraphQLScalarType INSTANCE; + + + static { + Coercing coercing = new Coercing() { + + private final Pattern HEX_PATTERN = Pattern.compile("^(#([A-Fa-f0-9]{3,4}){1,2})$"); + + @Override + public String serialize(Object input) throws CoercingSerializeException { + Color color = parseColor(input, CoercingSerializeException::new); + boolean hasAlpha = color.getAlpha() != 255; + if (hasAlpha){ + return String.format("#%02x%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha()); + } else { + return String.format("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue()); + } + } + + @Override + public Color parseValue(Object input) throws CoercingParseValueException { + return parseColor(input, CoercingParseValueException::new); + } + + @Override + public Color parseLiteral(Object input) throws CoercingParseLiteralException { + if (!(input instanceof StringValue)) { + throw new CoercingParseLiteralException("Expected type 'StringValue' but was '" + typeName(input) + "'."); + } + String stringValue = ((StringValue) input).getValue(); + return parseColor(stringValue, CoercingParseLiteralException::new); + } + + @Override + public Value valueToLiteral(Object input) { + String s = serialize(input); + return StringValue.newStringValue(s).build(); + } + + + private Color parseColor(Object input, Function exceptionMaker) { + final Color result; + if (input instanceof Color) { + result = (Color) input; + } else if (input instanceof String) { + try { + String hex = ((String) input); + + //validation + //regex + if (!HEX_PATTERN.matcher(hex).matches()) { + throw new IllegalArgumentException("Invalid hex color code value : '" + input + "'."); + } + + int i = Integer.decode(hex); + int inputLength = hex.length(); + + if (inputLength == 4) { + // #RGB + result = new Color( + (i >> 8 & 0xF) * 0x11, + (i >> 4 & 0xF) * 0x11, + (i & 0xF) * 0x11 + ); + } else if (inputLength == 5) { + // #RGBA + result = new Color( + (i >> 12 & 0xF) * 0x11, + (i >> 8 & 0xF) * 0x11, + (i >> 4 & 0xF) * 0x11, + (i & 0xF) * 0x11 + ); + } else if (inputLength == 7) { + // #RRGGBB + result = new Color(i); + } else { + // #RRGGBBAA + result = new Color( + (i >> 24 & 0xFF), + (i >> 16 & 0xFF), + (i >> 8 & 0xFF), + (i & 0xFF) + ); + } + } catch (NullPointerException | IllegalArgumentException ex) { + throw exceptionMaker.apply("Invalid hex color code value : '" + input + "'. because of : '" + ex.getMessage() + "'"); + } + } else { + throw exceptionMaker.apply("Expected a 'String' or 'Color' but was '" + typeName(input) + "'."); + } + return result; + } + + }; + + INSTANCE = GraphQLScalarType.newScalar() + .name("HexColorCode") + .description("A field whose value is a hexadecimal color code: https://en.wikipedia.org/wiki/Web_colors.") + .coercing(coercing).build(); + } + +} diff --git a/src/test/groovy/graphql/scalars/color/hex/HexColorCodeScalarTest.groovy b/src/test/groovy/graphql/scalars/color/hex/HexColorCodeScalarTest.groovy new file mode 100644 index 0000000..97c6e18 --- /dev/null +++ b/src/test/groovy/graphql/scalars/color/hex/HexColorCodeScalarTest.groovy @@ -0,0 +1,92 @@ +package graphql.scalars.color.hex + +import graphql.language.StringValue +import graphql.scalars.ExtendedScalars +import graphql.schema.CoercingParseValueException +import spock.lang.Specification +import spock.lang.Unroll + +import static graphql.scalars.util.TestKit.mkColor + +class HexColorCodeScalarTest extends Specification { + + def coercing = ExtendedScalars.HexColorCode.getCoercing() + + @Unroll + def "invoke parseValue for hexCode"() { + when: + def result = coercing.parseValue(input) + then: + result.equals(expectedValue) + where: + input | expectedValue + "#ff0000" | mkColor(0xff, 0, 0) + "#123" | mkColor(0x11, 0x22, 0x33) + "#11223344" | mkColor(0x11, 0x22, 0x33, 0x44) + "#1234" | mkColor(0x11, 0x22, 0x33, 0x44) + } + + @Unroll + def "invoke parseLiteral for hexCode"() { + when: + def result = coercing.parseLiteral(input) + then: + result == expectedValue + where: + input | expectedValue + new StringValue("#ff0000") | mkColor(0xff, 0, 0) + new StringValue("#123") | mkColor(0x11, 0x22, 0x33) + new StringValue("#11223344") | mkColor(0x11, 0x22, 0x33, 0x44) + new StringValue("#1234") | mkColor(0x11, 0x22, 0x33, 0x44) + } + + @Unroll + def "invoke serialize with hexCode"() { + when: + def result = coercing.serialize(input) + then: + result == expectedValue + where: + input | expectedValue + "#ff0000" | "#ff0000" + "#123" | "#112233" + "#11223344" | "#11223344" + "#1234" | "#11223344" + mkColor(0x21, 0x23, 0x33) | "#212333" + mkColor(0x21, 0x23, 0x33, 0x44) | "#21233344" + } + + @Unroll + def "invoke valueToLiteral with hexCode"() { + when: + def result = coercing.valueToLiteral(input) + then: + result.isEqualTo(expectedValue) + where: + input | expectedValue + "#ff0000" | new StringValue("#ff0000") + "#123" | new StringValue("#112233") + "#11223344" | new StringValue("#11223344") + "#1234" | new StringValue("#11223344") + mkColor(0x21, 0x23, 0x33) | new StringValue("#212333") + mkColor(0x21, 0x23, 0x33, 0x44) | new StringValue("#21233344") + } + + @Unroll + def "parseValue throws exception for invalid input #value"() { + when: + def result = coercing.parseValue(input) + then: + thrown(CoercingParseValueException) + where: + input | _ + "ff000" | _ + "" | _ + "not a hex code" | _ + "42.3" | _ + new Double(42.3) | _ + new Float(42.3) | _ + new Object() | _ + } + +} \ No newline at end of file diff --git a/src/test/groovy/graphql/scalars/util/TestKit.groovy b/src/test/groovy/graphql/scalars/util/TestKit.groovy index 2a96dee..e5acaac 100644 --- a/src/test/groovy/graphql/scalars/util/TestKit.groovy +++ b/src/test/groovy/graphql/scalars/util/TestKit.groovy @@ -5,6 +5,7 @@ import graphql.language.IntValue import graphql.language.StringValue import graphql.scalars.country.code.CountryCode +import java.awt.Color import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime @@ -97,4 +98,11 @@ class TestKit { static CountryCode mkCountryCode(String countryCode) { return CountryCode.valueOf(countryCode) } -} + + static Color mkColor(int r, int g, int b, int a) { + return new Color(r, g, b, a) + } + static Color mkColor(int r, int g, int b) { + return new Color(r, g, b) + } +} \ No newline at end of file