From acefe6500047e3a5fbf213f645ad3a88076c5d02 Mon Sep 17 00:00:00 2001 From: Sebastian Toepfer <61313468+sebastian-toepfer@users.noreply.github.com> Date: Tue, 3 Dec 2024 17:03:53 +0100 Subject: [PATCH] add support for email format --- .../vocabulary/formatassertion/Formats.java | 23 ++-- .../formatassertion/rfc/Rfc5321.java | 51 ++++++++ .../src/main/resources/rfc/rfc5321 | 117 ++++++++++++++++++ .../formatassertion/FormatsTest.java | 9 ++ 4 files changed, 192 insertions(+), 8 deletions(-) create mode 100644 vocabulary/format-assertion/src/main/java/io/github/sebastiantoepfer/jsonschema/vocabulary/formatassertion/rfc/Rfc5321.java create mode 100644 vocabulary/format-assertion/src/main/resources/rfc/rfc5321 diff --git a/vocabulary/format-assertion/src/main/java/io/github/sebastiantoepfer/jsonschema/vocabulary/formatassertion/Formats.java b/vocabulary/format-assertion/src/main/java/io/github/sebastiantoepfer/jsonschema/vocabulary/formatassertion/Formats.java index 7cc6542..311f362 100644 --- a/vocabulary/format-assertion/src/main/java/io/github/sebastiantoepfer/jsonschema/vocabulary/formatassertion/Formats.java +++ b/vocabulary/format-assertion/src/main/java/io/github/sebastiantoepfer/jsonschema/vocabulary/formatassertion/Formats.java @@ -25,21 +25,28 @@ import io.github.sebastiantoepfer.jsonschema.vocabulary.formatassertion.rfc.Rfc; import io.github.sebastiantoepfer.jsonschema.vocabulary.formatassertion.rfc.Rfc3339; +import io.github.sebastiantoepfer.jsonschema.vocabulary.formatassertion.rfc.Rfc5321; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.stream.Stream; final class Formats { - private static final List RFCS = Map.ofEntries( - Map.entry("date-time", "date-time"), - Map.entry("date", "full-date"), - Map.entry("time", "full-time"), - Map.entry("duration", "duration") + private static final List RFCS = Stream.concat( + Map.ofEntries( + Map.entry("date-time", "date-time"), + Map.entry("date", "full-date"), + Map.entry("time", "full-time"), + Map.entry("duration", "duration") + ) + .entrySet() + .stream() + .map(entry -> new RfcBasedFormat(entry.getKey(), new Rfc3339(), entry.getValue())), + Stream.of( + new RfcBasedFormat("email", new Rfc5321(), "mailbox") + ) ) - .entrySet() - .stream() - .map(entry -> new RfcBasedFormat(entry.getKey(), new Rfc3339(), entry.getValue())) .map(Format.class::cast) .toList(); diff --git a/vocabulary/format-assertion/src/main/java/io/github/sebastiantoepfer/jsonschema/vocabulary/formatassertion/rfc/Rfc5321.java b/vocabulary/format-assertion/src/main/java/io/github/sebastiantoepfer/jsonschema/vocabulary/formatassertion/rfc/Rfc5321.java new file mode 100644 index 0000000..1829e5b --- /dev/null +++ b/vocabulary/format-assertion/src/main/java/io/github/sebastiantoepfer/jsonschema/vocabulary/formatassertion/rfc/Rfc5321.java @@ -0,0 +1,51 @@ +/* + * The MIT License + * + * Copyright 2024 sebastian. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package io.github.sebastiantoepfer.jsonschema.vocabulary.formatassertion.rfc; + +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; + +public final class Rfc5321 implements Rfc { + + private static final Map RULES = Map.of( + "mailbox", + Pattern.compile( + "^(([A-Za-z0-9!#$%&'*+-/=?\\^_`{|}~]+" + + "(\\.[A-Za-z0-9!#$%&'*+-/=?\\^_`{|}~]+)*|\"" + + "(([ -\\!#-\\[]|[\\]-~])|\\\\[ -~])*\")@([A-Za-z0-9](([-A-Za-z0-9]*[A-Za-z0-9]))?" + + "(\\.[A-Za-z0-9](([-A-Za-z0-9]*[A-Za-z0-9]))?)*|(\\[(\\d{1,3}(\\.\\d{1,3}){3}|[Ii][Pp][Vv]6\\:" + + "([0-9A-Fa-f]{1,4}(\\:[0-9A-Fa-f]{1,4}){7}|([0-9A-Fa-f]{1,4}(\\:[0-9A-Fa-f]{1,4}){0,5})?\\:\\:" + + "([0-9A-Fa-f]{1,4}(\\:[0-9A-Fa-f]{1,4}){0,5})?|[0-9A-Fa-f]{1,4}(\\:[0-9A-Fa-f]{1,4}){5}\\:\\d{1,3}" + + "(\\.\\d{1,3}){3}|([0-9A-Fa-f]{1,4}(\\:[0-9A-Fa-f]{1,4}){0,3})?\\:\\:([0-9A-Fa-f]{1,4}" + + "(\\:[0-9A-Fa-f]{1,4}){0,3}\\:)?\\d{1,3}(\\.\\d{1,3}){3})|([-A-Za-z0-9]*[A-Za-z0-9])" + + "\\:[\\!-Z\\^-~]+)\\])))$" + ) + ); + + @Override + public Optional findRuleByName(final String ruleName) { + return Optional.ofNullable(RULES.get(ruleName)).map(RegExRule::new); + } +} diff --git a/vocabulary/format-assertion/src/main/resources/rfc/rfc5321 b/vocabulary/format-assertion/src/main/resources/rfc/rfc5321 new file mode 100644 index 0000000..dc31438 --- /dev/null +++ b/vocabulary/format-assertion/src/main/resources/rfc/rfc5321 @@ -0,0 +1,117 @@ +atext = ALPHA / DIGIT / ; Printable US-ASCII + "!" / "#" / ; characters not including + "$" / "%" / ; specials. Used for atoms. + "&" / "'" / + "*" / "+" / + "-" / "/" / + "=" / "?" / + "^" / "_" / + "`" / "{" / + "|" / "}" / + "~" +IPv4-address-literal = Snum 3("." Snum) + +IPv6-address-literal = "IPv6:" IPv6-addr + +General-address-literal = Standardized-tag ":" 1*dcontent + +Standardized-tag = Ldh-str + ; Standardized-tag MUST be specified in a + ; Standards-Track RFC and registered with IANA + +dcontent = %d33-90 / ; Printable US-ASCII + %d94-126 ; excl. "[", "\", "]" + +Snum = 1*3DIGIT + ; representing a decimal integer + ; value in the range 0 through 255 + +IPv6-addr = IPv6-full / IPv6-comp / IPv6v4-full / IPv6v4-comp + +IPv6-hex = 1*4HEXDIG + +IPv6-full = IPv6-hex 7(":" IPv6-hex) + +IPv6-comp = [IPv6-hex *5(":" IPv6-hex)] "::" + [IPv6-hex *5(":" IPv6-hex)] + ; The "::" represents at least 2 16-bit groups of + ; zeros. No more than 6 groups in addition to the + ; "::" may be present. + +IPv6v4-full = IPv6-hex 5(":" IPv6-hex) ":" IPv4-address-literal + +IPv6v4-comp = [IPv6-hex *3(":" IPv6-hex)] "::" + [IPv6-hex *3(":" IPv6-hex) ":"] + IPv4-address-literal + ; The "::" represents at least 2 16-bit groups of + ; zeros. No more than 4 groups in addition to the + ; "::" and IPv4-address-literal may be present. +Reverse-path = Path / "<>" + +Forward-path = Path + +Path = "<" [ A-d-l ":" ] Mailbox ">" + +A-d-l = At-domain *( "," At-domain ) + ; Note that this form, the so-called "source + ; route", MUST BE accepted, SHOULD NOT be + ; generated, and SHOULD be ignored. + +At-domain = "@" Domain + +Mail-parameters = esmtp-param *(SP esmtp-param) + +Rcpt-parameters = esmtp-param *(SP esmtp-param) + +esmtp-param = esmtp-keyword ["=" esmtp-value] + +esmtp-keyword = (ALPHA / DIGIT) *(ALPHA / DIGIT / "-") + +esmtp-value = 1*(%d33-60 / %d62-126) + ; any CHAR excluding "=", SP, and control + ; characters. If this string is an email address, + ; i.e., a Mailbox, then the "xtext" syntax [32] + ; SHOULD be used. + +Keyword = Ldh-str + +Argument = Atom + +Domain = sub-domain *("." sub-domain) + +sub-domain = Let-dig [Ldh-str] + +Let-dig = ALPHA / DIGIT + +Ldh-str = *( ALPHA / DIGIT / "-" ) Let-dig + +address-literal = "[" ( IPv4-address-literal / + IPv6-address-literal / + General-address-literal ) "]" + ; See Section 4.1.3 + +Mailbox = Local-part "@" ( Domain / address-literal ) + +Local-part = Dot-string / Quoted-string + ; MAY be case-sensitive + + +Dot-string = Atom *("." Atom) + +Atom = 1*atext + +Quoted-string = DQUOTE *QcontentSMTP DQUOTE + +QcontentSMTP = qtextSMTP / quoted-pairSMTP + +quoted-pairSMTP = %d92 %d32-126 + ; i.e., backslash followed by any ASCII + ; graphic (including itself) or SPace + +qtextSMTP = %d32-33 / %d35-91 / %d93-126 + ; i.e., within a quoted string, any + ; ASCII graphic or space is permitted + ; without blackslash-quoting except + ; double-quote and the backslash itself. + +String = Atom / Quoted-string \ No newline at end of file diff --git a/vocabulary/format-assertion/src/test/java/io/github/sebastiantoepfer/jsonschema/vocabulary/formatassertion/FormatsTest.java b/vocabulary/format-assertion/src/test/java/io/github/sebastiantoepfer/jsonschema/vocabulary/formatassertion/FormatsTest.java index 4dec5bd..e23a873 100644 --- a/vocabulary/format-assertion/src/test/java/io/github/sebastiantoepfer/jsonschema/vocabulary/formatassertion/FormatsTest.java +++ b/vocabulary/format-assertion/src/test/java/io/github/sebastiantoepfer/jsonschema/vocabulary/formatassertion/FormatsTest.java @@ -59,4 +59,13 @@ void should_found_durationFormat() { assertThat(new Formats().findByName("duration").applyTo("PT1H10M"), is(true)); assertThat(new Formats().findByName("duration").applyTo("noDuration"), is(false)); } + + @Test + void should_found_emailFormat() { + assertThat( + new Formats().findByName("email").applyTo("61313468+sebastian-toepfer@users.noreply.github.com"), + is(true) + ); + assertThat(new Formats().findByName("email").applyTo("noEmail"), is(false)); + } }