From c994bbf782c491bc035d4c927dfcb9c3084b8bd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Berg=20Glasius?= Date: Tue, 6 Feb 2024 12:16:51 +0100 Subject: [PATCH] Testing, release artefacts, cleanup and README.md --- .github/workflows/build.yml | 28 ++++ .github/workflows/release.yml | 64 ++++++++ build.gradle | 116 ++++++++++--- gradle.properties | 2 +- grails-app/i18n/messages.properties | 2 +- .../phoneconstraint/Application.groovy | 5 +- .../glasius/phoneconstraint/BootStrap.groovy | 9 -- grails-wrapper.jar | Bin 5507 -> 0 bytes grailsw | 152 ------------------ grailsw.bat | 89 ---------- settings.gradle | 2 +- .../GrailsPhoneConstraintGrailsPlugin.groovy | 33 ---- ...lsPhoneNumberConstraintGrailsPlugin.groovy | 22 +++ .../PhoneNumberConstraint.groovy | 81 ++++++---- .../phoneconstraint/PhoneNumberUtil.groovy | 4 + .../PhoneNumberConstraintSpec.groovy | 151 ++++++++++++++--- .../ValidateablePhoneNumber.groovy | 12 ++ .../ValidateableProperty.groovy | 7 - 18 files changed, 416 insertions(+), 363 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/release.yml delete mode 100644 grails-app/init/dk/glasius/phoneconstraint/BootStrap.groovy delete mode 100644 grails-wrapper.jar delete mode 100755 grailsw delete mode 100755 grailsw.bat delete mode 100644 src/main/groovy/dk/glasius/phoneconstraint/GrailsPhoneConstraintGrailsPlugin.groovy create mode 100644 src/main/groovy/dk/glasius/phoneconstraint/GrailsPhoneNumberConstraintGrailsPlugin.groovy create mode 100644 src/test/groovy/dk/glasius/phoneconstraint/ValidateablePhoneNumber.groovy delete mode 100644 src/test/groovy/dk/glasius/phoneconstraint/ValidateableProperty.groovy diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..bb4846b --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,28 @@ +# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Build + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 8 + uses: actions/setup-java@v2 + with: + java-version: '8' + distribution: 'adopt' + cache: gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..40062b8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,64 @@ +name: Release +on: + release: + types: [ published ] +jobs: + release: + runs-on: ubuntu-latest + env: + GIT_USER_NAME: ${{ secrets.GIT_USER_NAME }} + GIT_USER_EMAIL: ${{ secrets.GIT_USER_EMAIL }} + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + token: ${{ secrets.GH_TOKEN }} + - uses: gradle/wrapper-validation-action@v1 + - name: Set up JDK + uses: actions/setup-java@v1 + with: + java-version: 8 + - name: Get latest release version number + id: get_version + uses: battila7/get-version-action@v2 + - name: Run pre-release + uses: micronaut-projects/github-actions/pre-release@master + with: + token: ${{ secrets.GITHUB_TOKEN }} + - name: Publish to Sonatype OSSRH + env: + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} + SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} + SIGNING_PASSPHRASE: ${{ secrets.SIGNING_PASSPHRASE }} + SECRING_FILE: ${{ secrets.SECRING_FILE }} + RELEASE_VERSION: ${{ steps.get_version.outputs.version-without-v }} + run: | + echo "${SECRING_FILE}" | base64 -d > "${GITHUB_WORKSPACE}/secring.gpg" + echo "Publishing Artifacts for $RELEASE_VERSION" + (set -x; ./gradlew -Pversion="${RELEASE_VERSION}" -Psigning.secretKeyRingFile="${GITHUB_WORKSPACE}/secring.gpg" publishToSonatype closeAndReleaseSonatypeStagingRepository --no-daemon) + - name: Bump patch version by one + uses: actions-ecosystem/action-bump-semver@v1 + id: bump_semver + with: + current_version: ${{steps.get_version.outputs.version-without-v }} + level: patch + - name: Set version in gradle.properties + env: + NEXT_VERSION: ${{ steps.bump_semver.outputs.new_version }} + run: | + echo "Preparing next snapshot" + ./gradlew snapshotVersion -Pversion="${NEXT_VERSION}" + - name: Commit & Push changes + uses: actions-js/push@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + author_name: ${{ secrets.GIT_USER_NAME }} + author_email: $${ secrets.GIT_USER_EMAIL }} + message: 'Set version to next SNAPSHOT' + - name: Run post-release + if: success() + uses: micronaut-projects/github-actions/post-release@master + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/build.gradle b/build.gradle index 1c6473a..c026b66 100644 --- a/build.gradle +++ b/build.gradle @@ -7,32 +7,26 @@ buildscript { classpath "org.grails:grails-gradle-plugin:$grailsGradlePluginVersion" } } +plugins { + id("io.github.gradle-nexus.publish-plugin") version "1.3.0" + id 'maven-publish' + id 'signing' +} +group "io.github.gpc" -version "0.1" -group "dk.glasius.phone" - -apply plugin:"org.grails.grails-plugin" +apply plugin: "org.grails.grails-plugin" repositories { mavenCentral() maven { url "https://repo.grails.org/grails/core" } } -configurations { - developmentOnly - runtimeClasspath { - extendsFrom developmentOnly - } -} - dependencies { - developmentOnly("org.springframework.boot:spring-boot-devtools") - console "org.grails:grails-console" implementation "org.springframework.boot:spring-boot-starter-logging" implementation "org.springframework.boot:spring-boot-starter-validation" implementation "org.springframework.boot:spring-boot-autoconfigure" implementation "org.grails:grails-core" - + compileOnly "org.grails:grails-plugin-domain-class" implementation 'com.googlecode.libphonenumber:libphonenumber:8.13.29' @@ -45,23 +39,105 @@ dependencies { bootRun { ignoreExitValue true jvmArgs( - '-Dspring.output.ansi.enabled=always', - '-noverify', - '-XX:TieredStopAtLevel=1', - '-Xmx1024m') + '-Dspring.output.ansi.enabled=always', + '-noverify', + '-XX:TieredStopAtLevel=1', + '-Xmx1024m') sourceResources sourceSets.main String springProfilesActive = 'spring.profiles.active' systemProperty springProfilesActive, System.getProperty(springProfilesActive) } -tasks.withType(GroovyCompile) { +tasks.withType(GroovyCompile).configureEach { configure(groovyOptions) { forkOptions.jvmArgs = ['-Xmx1024m'] } } -tasks.withType(Test) { +tasks.withType(Test).configureEach { useJUnitPlatform() } // enable if you wish to package this plugin as a standalone application bootJar.enabled = false + +publishing { + publications { + maven(MavenPublication) { + groupId = project.group + artifactId = 'phone-number-constraint' + version = project.version + + from components.java + artifact sourcesJar + artifact javadocJar + + pom { + name = 'phone-number-constraint' + description = "This plugin establishes a 'phoneNumber' constraint for validateable objects." + url = 'https://github.com/gpc/grails-phone-number-constraint' + licenses { + license { + name = 'The Apache License, Version 2.0' + url = 'https://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + developers { + developer { + id = 'sbglasius' + name = 'Søren Berg Glasius' + email = 'soeren@glasius.dk' + } + } + scm { + connection = 'scm:git:git://github.com/gpc/grails-phone-number-constraint.git' + developerConnection = 'scm:git:ssh://github.com:gpc/grails-phone-number-constraint.git' + url = 'https://github.com/gpc/grails-phone-number-constraint/tree/main' + } + } + } + } +} + +ext."signing.keyId" = project.findProperty('signing.keyId') ?: System.getenv('SIGNING_KEY_ID') +ext."signing.password" = project.findProperty('signing.password') ?: System.getenv('SIGNING_PASSPHRASE') +ext."signing.secretKeyRingFile" = project.findProperty('signing.secretKeyRingFile') ?: (System.getenv('SIGNING_PASSPHRASE') ?: "${System.getProperty('user.home')}/.gnupg/secring.gpg") + +ext.isReleaseVersion = !version.endsWith("SNAPSHOT") + +afterEvaluate { + signing { + required { isReleaseVersion } + sign publishing.publications.maven + } +} + +tasks.withType(Sign).configureEach { + onlyIf { isReleaseVersion } +} + +nexusPublishing { + repositories { + sonatype { + def ossUser = System.getenv('SONATYPE_USERNAME') ?: project.findProperty('sonatypeOss2Username') ?: '' + def ossPass = System.getenv('SONATYPE_PASSWORD') ?: project.findProperty("sonatypeOss2Password") ?: '' + def ossStagingProfileId = System.getenv('SONATYPE_STAGING_PROFILE_ID') ?: project.findProperty("sonatypeOssStagingProfileId") ?: '' + + nexusUrl = uri("https://s01.oss.sonatype.org/service/local/") + snapshotRepositoryUrl = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") + username = ossUser + password = ossPass + stagingProfileId = ossStagingProfileId + } + } +} + +tasks.register('snapshotVersion') { + doLast { + if (!project.version.endsWith('-SNAPSHOT')) { + ant.propertyfile(file: "gradle.properties") { + entry(key: "version", value: "${project.version}-SNAPSHOT") + } + } + } +} + diff --git a/gradle.properties b/gradle.properties index 377f317..760721d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ grailsVersion=5.3.6 grailsGradlePluginVersion=5.3.1 groovyVersion=3.0.11 -gorm.version=7.3.4 +version=1.0.0-SNAPSHOT org.gradle.daemon=true org.gradle.parallel=true org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx1024M diff --git a/grails-app/i18n/messages.properties b/grails-app/i18n/messages.properties index 857fd10..acd17cf 100644 --- a/grails-app/i18n/messages.properties +++ b/grails-app/i18n/messages.properties @@ -1 +1 @@ -default.invalid.phone.message=Property [{0}] of class [{1}] with value [{2}] does not pass phone number validation +default.invalid.phoneNumber.message=Property [{0}] of class [{1}] with value [{2}] does not pass phone-number validation diff --git a/grails-app/init/dk/glasius/phoneconstraint/Application.groovy b/grails-app/init/dk/glasius/phoneconstraint/Application.groovy index 64eea1b..7a0796f 100644 --- a/grails-app/init/dk/glasius/phoneconstraint/Application.groovy +++ b/grails-app/init/dk/glasius/phoneconstraint/Application.groovy @@ -1,11 +1,12 @@ package dk.glasius.phoneconstraint -import grails.boot.* +import grails.boot.GrailsApp import grails.boot.config.GrailsAutoConfiguration -import grails.plugins.metadata.* +import grails.plugins.metadata.PluginSource @PluginSource class Application extends GrailsAutoConfiguration { + static void main(String[] args) { GrailsApp.run(Application, args) } diff --git a/grails-app/init/dk/glasius/phoneconstraint/BootStrap.groovy b/grails-app/init/dk/glasius/phoneconstraint/BootStrap.groovy deleted file mode 100644 index b5f9fd8..0000000 --- a/grails-app/init/dk/glasius/phoneconstraint/BootStrap.groovy +++ /dev/null @@ -1,9 +0,0 @@ -package dk.glasius.phoneconstraint - -class BootStrap { - - def init = { servletContext -> - } - def destroy = { - } -} diff --git a/grails-wrapper.jar b/grails-wrapper.jar deleted file mode 100644 index 2cb677725e6d8d38c7a2ba03ca34b6703357dcb9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5507 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 \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -JAR_PATH=$APP_HOME/grails-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - JAVACMD=`cygpath --unix "$JAVACMD"` - JAR_PATH=`cygpath --path --mixed "$JAR_PATH"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRAILS_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRAILS_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Split up the JVM_OPTS And GRAILS_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRAILS_OPTS - -exec "$JAVACMD" -jar "${JVM_OPTS[@]}" "$JAR_PATH" "$@" diff --git a/grailsw.bat b/grailsw.bat deleted file mode 100755 index 14734e4..0000000 --- a/grailsw.bat +++ /dev/null @@ -1,89 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Grails startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRAILS_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-XX:+TieredCompilation" "-XX:TieredStopAtLevel=1" "-XX:CICompilerCount=3" - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line -set JAR_PATH=%APP_HOME%/grails-wrapper.jar - -@rem Execute Grails -"%JAVA_EXE%" -jar %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRAILS_OPTS% %JAR_PATH% %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRAILS_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRAILS_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/settings.gradle b/settings.gradle index 6c027f1..0d79951 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'grails-phone-constraint' +rootProject.name = 'grails-phone-number-constraint' diff --git a/src/main/groovy/dk/glasius/phoneconstraint/GrailsPhoneConstraintGrailsPlugin.groovy b/src/main/groovy/dk/glasius/phoneconstraint/GrailsPhoneConstraintGrailsPlugin.groovy deleted file mode 100644 index 6029b0f..0000000 --- a/src/main/groovy/dk/glasius/phoneconstraint/GrailsPhoneConstraintGrailsPlugin.groovy +++ /dev/null @@ -1,33 +0,0 @@ -package dk.glasius.phoneconstraint - -import grails.plugins.* - -class GrailsPhoneConstraintGrailsPlugin extends Plugin { - - def grailsVersion = "5.3.0 > *" - def pluginExcludes = [ - "grails-app/views/error.gsp" - ] - - // TODO Fill in these fields - def title = "Grails Phone Constraint" // Headline display name of the plugin - def author = "Søren Berg Glasius" - def authorEmail = "soeren@glasius.dk" - def description = '''\ -Validating constraint for phone numbers using libphonenumber -''' - - // URL to the plugin's documentation - def documentation = "http://grails.org/plugin/grails-phone-constraint" - - def license = "APACHE" - -// def issueManagement = [ system: "JIRA", url: "http://jira.grails.org/browse/GPMYPLUGIN" ] - - // Online location of the plugin's browseable source code. -// def scm = [ url: "http://svn.codehaus.org/grails-plugins/" ] - - void doWithApplicationContext() { - PhoneNumberConstraintRegistration.register(applicationContext) - } -} diff --git a/src/main/groovy/dk/glasius/phoneconstraint/GrailsPhoneNumberConstraintGrailsPlugin.groovy b/src/main/groovy/dk/glasius/phoneconstraint/GrailsPhoneNumberConstraintGrailsPlugin.groovy new file mode 100644 index 0000000..0f217ea --- /dev/null +++ b/src/main/groovy/dk/glasius/phoneconstraint/GrailsPhoneNumberConstraintGrailsPlugin.groovy @@ -0,0 +1,22 @@ +package dk.glasius.phoneconstraint + +import grails.plugins.Plugin + +class GrailsPhoneNumberConstraintGrailsPlugin extends Plugin { + + def grailsVersion = "5.3.0 > *" + + def title = 'Grails Phone Constraint' + def author = 'Søren Berg Glasius' + def authorEmail = 'soeren@glasius.dk' + def description = 'Validating constraint for phone numbers using libphonenumber' + + def documentation = 'https://github.com/gpc/grails-phone-number-constraint' + def license = 'APACHE' + def issueManagement = [system: 'GITHUB', url: 'https://github.com/gpc/grails-phone-number-constraint/issues'] + def scm = [url: 'https://github.com/gpc/grails-phone-number-constraint'] + + void doWithApplicationContext() { + PhoneNumberConstraintRegistration.register(applicationContext) + } +} diff --git a/src/main/groovy/dk/glasius/phoneconstraint/PhoneNumberConstraint.groovy b/src/main/groovy/dk/glasius/phoneconstraint/PhoneNumberConstraint.groovy index 84b06d1..781a6e6 100644 --- a/src/main/groovy/dk/glasius/phoneconstraint/PhoneNumberConstraint.groovy +++ b/src/main/groovy/dk/glasius/phoneconstraint/PhoneNumberConstraint.groovy @@ -1,60 +1,82 @@ package dk.glasius.phoneconstraint -import static com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat - +import grails.databinding.SimpleDataBinder +import grails.databinding.SimpleMapDataBindingSource +import grails.gorm.validation.ConstrainedProperty import groovy.transform.CompileStatic import org.grails.datastore.gorm.validation.constraints.AbstractConstraint import org.springframework.context.MessageSource import org.springframework.validation.Errors +import static com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat + @CompileStatic class PhoneNumberConstraint extends AbstractConstraint { static final String PHONE_CONSTRAINT = "phoneNumber" + static final String DEFAULT_INVALID_PHONE_NUMBER_MESSAGE_CODE = 'default.invalid.phoneNumber.message' + static final String DEFAULT_INVALID_PHONE_NUMBER_CONSTRAINT = PHONE_CONSTRAINT + + protected boolean enabled = true + protected String defaultRegion = 'US' + protected PhoneNumberFormat phoneNumberFormat = PhoneNumberFormat.INTERNATIONAL + protected boolean doFormatting = false - boolean enabled = true - String defaultRegion = 'US' - PhoneNumberFormat phoneNumberFormat = PhoneNumberFormat.INTERNATIONAL - PhoneNumberConstraint(Class constraintOwningClass, String constraintPropertyName, Object constraintParameter, MessageSource messageSource) { super(constraintOwningClass, constraintPropertyName, constraintParameter, messageSource) enabled = validateParameter(constraintParameter) as Boolean } + @Override + boolean supports(Class type) { CharSequence.isAssignableFrom(type) } + + @Override + String getName() { PHONE_CONSTRAINT } + @Override protected Object validateParameter(Object constraintParameter) { if (isBoolean(constraintParameter)) { return constraintParameter - } else if (isLanguage(constraintParameter)) { + } else if (isRegionString(constraintParameter)) { defaultRegion = constraintParameter as String return true } else if (isMapConfig(constraintParameter)) { Map config = constraintParameter as Map defaultRegion = config.region ?: defaultRegion - phoneNumberFormat = parseNumberFormat(config.numberFormat) ?: phoneNumberFormat - return config.region || config.numberFormat + phoneNumberFormat = PhoneNumberUtil.parseNumberFormat(config.numberFormat) ?: phoneNumberFormat + doFormatting = config.format != null ? config.format as Boolean : doFormatting + return config.region || config.numberFormat || config.format != null } - throw new IllegalArgumentException("Parameter for constraint [$PHONE_CONSTRAINT] of property [$constraintPropertyName] of class [$constraintOwningClass] must be a boolean, a language string or a config map") + throw new IllegalArgumentException("Parameter for constraint [$PHONE_CONSTRAINT] of property [$constraintPropertyName] of class [$constraintOwningClass] must be a boolean, a [region] string or a config map with keys [region, numberFormat, format]") } @Override protected void processValidate(Object target, Object propertyValue, Errors errors) { + if (enabled) { + if (!PhoneNumberUtil.isValid(propertyValue as String, defaultRegion)) { + Object[] args = [constraintPropertyName, constraintOwningClass, propertyValue] + rejectValue(target, errors, DEFAULT_INVALID_PHONE_NUMBER_MESSAGE_CODE, + DEFAULT_INVALID_PHONE_NUMBER_CONSTRAINT + ConstrainedProperty.INVALID_SUFFIX, args) + } else { + if (doFormatting) { + String value = PhoneNumberUtil.format(propertyValue as String, defaultRegion, phoneNumberFormat) + SimpleMapDataBindingSource source = new SimpleMapDataBindingSource([(constraintPropertyName): value]) + new SimpleDataBinder().bind(target, source) + } + } + } } - @Override - boolean supports(Class type) { - CharSequence.isAssignableFrom(type) - } - - @Override - String getName() { - PHONE_CONSTRAINT - } - - private static boolean isLanguage(constraintParameter) { - constraintParameter instanceof String && constraintParameter?.size() == 2 + private static boolean isRegionString(constraintParameter) { + if (constraintParameter instanceof String) { + if (!PhoneNumberUtil.isValidRegionCode(constraintParameter as String)) { + throw new IllegalArgumentException("Wrong [region] string. [$constraintParameter] is not supported") + } + return true + } + return false } private static boolean isBoolean(constraintParameter) { @@ -64,20 +86,19 @@ class PhoneNumberConstraint extends AbstractConstraint { private static boolean isMapConfig(constraintParameter) { if (constraintParameter instanceof Map) { Map config = constraintParameter as Map - if(config.region && !PhoneNumberUtil.isValidRegionCode(config.region)) { - throw new IllegalArgumentException("Wrong config parameter [region]. [$config.region] is not supported") + if (config.region && !PhoneNumberUtil.isValidRegionCode(config.region)) { + throw new IllegalArgumentException("Wrong config parameter [region]. [$config.region] is not supported") } - if(config.numberFormat && !parseNumberFormat(config.numberFormat)) { - throw new IllegalArgumentException("Wrong config parameter [numberFormat]. Must be a value from [com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat]") + if (config.numberFormat && !PhoneNumberUtil.parseNumberFormat(config.numberFormat)) { + throw new IllegalArgumentException("Wrong config parameter [numberFormat]. Must be one of [${PhoneNumberFormat.values()*.name().join(', ')}], but was [${config.numberFormat}]") + } + if (config.format != null && !(config.format instanceof Boolean)) { + throw new IllegalArgumentException("Wrong config parameter [format]. [$config.format] is not a boolean") } return true } return false } - - static PhoneNumberFormat parseNumberFormat(String format) { - PhoneNumberFormat.values().find { it.name() == format } - } } diff --git a/src/main/groovy/dk/glasius/phoneconstraint/PhoneNumberUtil.groovy b/src/main/groovy/dk/glasius/phoneconstraint/PhoneNumberUtil.groovy index d760c7a..9e89deb 100644 --- a/src/main/groovy/dk/glasius/phoneconstraint/PhoneNumberUtil.groovy +++ b/src/main/groovy/dk/glasius/phoneconstraint/PhoneNumberUtil.groovy @@ -24,6 +24,10 @@ class PhoneNumberUtil { PhoneNumberFormat phoneNumberFormat = defaultFormat ?: PhoneNumberFormat.INTERNATIONAL instance.format(phone, phoneNumberFormat) } + + static PhoneNumberFormat parseNumberFormat(String format) { + PhoneNumberFormat.values().find { it.name() == format } + } static boolean isValidRegionCode(String regionCode) { return regionCode != null && instance.supportedRegions.contains(regionCode) diff --git a/src/test/groovy/dk/glasius/phoneconstraint/PhoneNumberConstraintSpec.groovy b/src/test/groovy/dk/glasius/phoneconstraint/PhoneNumberConstraintSpec.groovy index 97f9c90..9fc93e8 100644 --- a/src/test/groovy/dk/glasius/phoneconstraint/PhoneNumberConstraintSpec.groovy +++ b/src/test/groovy/dk/glasius/phoneconstraint/PhoneNumberConstraintSpec.groovy @@ -1,24 +1,23 @@ package dk.glasius.phoneconstraint import grails.validation.ValidationErrors +import org.grails.testing.GrailsUnitTest +import org.springframework.context.support.StaticMessageSource +import org.springframework.validation.FieldError import spock.lang.Specification import spock.lang.Unroll import static com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat @Unroll -class PhoneNumberConstraintSpec extends Specification { +class PhoneNumberConstraintSpec extends Specification implements GrailsUnitTest { - PhoneNumberConstraint constraint - ValidationErrors errors = Mock() - - void setup() { - } + PhoneNumberConstraint constraint void "constraint name and config should set constraints config correct"() { when: constraint = new PhoneNumberConstraint( - ValidateableProperty, + ValidateablePhoneNumber, 'property', cfg, null @@ -29,22 +28,138 @@ class PhoneNumberConstraintSpec extends Specification { name == 'phoneNumber' enabled == expectEnabled defaultRegion == expectedRegion - phoneNumberFormat == expectedFormat + phoneNumberFormat == expectedNumberFormat + doFormatting == expectedDoFormatting } where: - cfg || expectEnabled | expectedRegion | expectedFormat - false || false | 'US' | PhoneNumberFormat.INTERNATIONAL - true || true | 'US' | PhoneNumberFormat.INTERNATIONAL - 'DK' || true | 'DK' | PhoneNumberFormat.INTERNATIONAL - [region: 'GB', numberFormat: 'NATIONAL'] || true | 'GB' | PhoneNumberFormat.NATIONAL - [region: 'GB'] || true | 'GB' | PhoneNumberFormat.INTERNATIONAL - [numberFormat: 'RFC3966'] || true | 'US' | PhoneNumberFormat.RFC3966 - [numberFormat: 'E164'] || true | 'US' | PhoneNumberFormat.E164 + cfg || expectEnabled | expectedRegion | expectedNumberFormat | expectedDoFormatting + false || false | 'US' | PhoneNumberFormat.INTERNATIONAL | false + true || true | 'US' | PhoneNumberFormat.INTERNATIONAL | false + 'DK' || true | 'DK' | PhoneNumberFormat.INTERNATIONAL | false + [region: 'GB', numberFormat: 'NATIONAL'] || true | 'GB' | PhoneNumberFormat.NATIONAL | false + [region: 'GB', format: true] || true | 'GB' | PhoneNumberFormat.INTERNATIONAL | true + [region: 'GB'] || true | 'GB' | PhoneNumberFormat.INTERNATIONAL | false + [numberFormat: 'RFC3966'] || true | 'US' | PhoneNumberFormat.RFC3966 | false + [numberFormat: 'E164'] || true | 'US' | PhoneNumberFormat.E164 | false + [format: true] || true | 'US' | PhoneNumberFormat.INTERNATIONAL | true } void "constraint config with errors"() { - + when: + constraint = new PhoneNumberConstraint( + ValidateablePhoneNumber, + 'property', + cfg, + null + ) + then: + IllegalArgumentException t = thrown() + t.message == expectedMessage + + where: + cfg || expectedMessage + 'DX' || 'Wrong [region] string. [DX] is not supported' + [region: 'GB', numberFormat: 'NATION'] || 'Wrong config parameter [numberFormat]. Must be one of [E164, INTERNATIONAL, NATIONAL, RFC3966], but was [NATION]' + [region: 'GB', numberFormat: 'NATIONAL', format: 'foo'] || 'Wrong config parameter [format]. [foo] is not a boolean' + [region: 'GX'] || 'Wrong config parameter [region]. [GX] is not supported' + [numberFormat: 'RFC3988'] || 'Wrong config parameter [numberFormat]. Must be one of [E164, INTERNATIONAL, NATIONAL, RFC3966], but was [RFC3988]' + [numberFormat: 'E112'] || 'Wrong config parameter [numberFormat]. Must be one of [E164, INTERNATIONAL, NATIONAL, RFC3966], but was [E112]' + [format: 'foo'] || 'Wrong config parameter [format]. [foo] is not a boolean' + } + + void "validate phoneNumber on bean with no errors"() { + given: + constraint = new PhoneNumberConstraint( + ValidateablePhoneNumber, + 'phoneNumber', + cfg, + null + ) + ValidateablePhoneNumber target = new ValidateablePhoneNumber(number as String) + ValidationErrors errors = Mock() + + when: + constraint.validate(target, number, errors) + + then: + 0 * errors.addError(_) + + where: + [cfg, number] << [ + [true, 'DK', [region: 'DK', numberFormat: 'NATIONAL']], + ['+4540404040', '+1 510 874 4567', '+15108744567', '+81 80-1234-5678', '+818012345678', '+61 4 1234 5678', '+61412345678', '+86 139 1099 8888', '+8613910998888'] + ].combinations() + } + + void "validate phoneNumber on bean with errors"() { + given: 'A default message in messageSource' + (messageSource as StaticMessageSource).addMessage('default.invalid.phoneNumber.message', Locale.default, 'Property [{0}] of class [{1}] with value [{2}] does not pass phone-number validation') + + and: 'a constraint' + constraint = new PhoneNumberConstraint( + ValidateablePhoneNumber, + 'phoneNumber', + cfg, + messageSource + ) + and: 'data to validate' + ValidateablePhoneNumber target = new ValidateablePhoneNumber(number as String) + + and: 'an errors object to add errors to' + ValidationErrors errors = Spy(new ValidationErrors(target)) + + when: + constraint.validate(target, number, errors) + + then: + 1 * errors.addError(_) >> { FieldError err -> + verifyAll(err) { + code == 'phoneNumber.invalid' + field == 'phoneNumber' + codes.size() == 20 + rejectedValue == number + defaultMessage == 'Property [{0}] of class [{1}] with value [{2}] does not pass phone-number validation' + } + } + + where: + [cfg, number] << [ + [true, 'DK', [region: 'DK', numberFormat: 'NATIONAL']], + ['+4544040', '+1 5 4567', '+151087', '+8078', '12345678', '1234 5678', '+6412345678', '+869 1099 8888', '+888'] + ].combinations() + } + + void "format phoneNumber"() { + given: + constraint = new PhoneNumberConstraint( + ValidateablePhoneNumber, + 'phoneNumber', + cfg, + null + ) + ValidateablePhoneNumber target = new ValidateablePhoneNumber(number as String) + ValidationErrors errors = Mock() + + when: + constraint.validate(target, number, errors) + + then: + 0 * errors.addError(_) + + and: + target.phoneNumber == expectedNumber + + where: + cfg | number || expectedNumber + [region: 'DK', numberFormat: 'NATIONAL', format: true] | '40506070' | '40 50 60 70' + [region: 'DK', numberFormat: 'NATIONAL', format: true] | '+4540506070' | '40 50 60 70' + [region: 'DK', format: true] | '40506070' | '+45 40 50 60 70' + [region: 'DK', format: true] | '+4540506070' | '+45 40 50 60 70' + [region: 'US', numberFormat: 'NATIONAL', format: true] | '+4540506070' | '40 50 60 70' + [region: 'US', numberFormat: 'NATIONAL', format: true] | '+15105551234' | '(510) 555-1234' + [region: 'US', numberFormat: 'INTERNATIONAL', format: true] | '+15105551234' | '+1 510-555-1234' + [region: 'US', numberFormat: 'RFC3966', format: true] | '+15105551234' | 'tel:+1-510-555-1234' + [region: 'US', numberFormat: 'E164', format: true] | '+15105551234' | '+15105551234' } - } diff --git a/src/test/groovy/dk/glasius/phoneconstraint/ValidateablePhoneNumber.groovy b/src/test/groovy/dk/glasius/phoneconstraint/ValidateablePhoneNumber.groovy new file mode 100644 index 0000000..61903e4 --- /dev/null +++ b/src/test/groovy/dk/glasius/phoneconstraint/ValidateablePhoneNumber.groovy @@ -0,0 +1,12 @@ +package dk.glasius.phoneconstraint + +import grails.validation.Validateable +import groovy.transform.Canonical +import groovy.transform.CompileStatic + +@Canonical +@CompileStatic +class ValidateablePhoneNumber implements Validateable { + + String phoneNumber +} \ No newline at end of file diff --git a/src/test/groovy/dk/glasius/phoneconstraint/ValidateableProperty.groovy b/src/test/groovy/dk/glasius/phoneconstraint/ValidateableProperty.groovy deleted file mode 100644 index 1f1d4c3..0000000 --- a/src/test/groovy/dk/glasius/phoneconstraint/ValidateableProperty.groovy +++ /dev/null @@ -1,7 +0,0 @@ -package dk.glasius.phoneconstraint - -import grails.validation.Validateable - -class ValidateableProperty implements Validateable { - String field -} \ No newline at end of file