diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 1e08044bb..abb744598 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -1,4 +1,4 @@ -name: Java CI +name: "Java CI" on: push: branches: @@ -9,72 +9,73 @@ on: env: GIT_USER_NAME: 'grails-build' GIT_USER_EMAIL: 'grails-build@users.noreply.github.com' - jobs: - test_project: runs-on: ubuntu-latest if: github.event_name == 'pull_request' steps: - - uses: actions/checkout@v4 - - uses: gradle/wrapper-validation-action@v2 - - uses: actions/setup-java@v4 + - name: "ðŸ“Ĩ Checkout repository" + uses: actions/checkout@v4 + - name: "☕ïļ Setup JDK" + uses: actions/setup-java@v4 with: - distribution: temurin java-version: 17 - - name: Run Tests - if: github.event_name == 'pull_request' - id: tests - uses: gradle/actions/setup-gradle@v3 - env: - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} - GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} + distribution: temurin + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@v4 with: - arguments: check -Dgeb.env=chromeHeadless -x test -x integrationTest - + develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + - name: "🏃 Run Tests" + env: + DEVELOCITY_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} + DEVELOCITY_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} + run: > + ./gradlew + -Dgeb.env=chromeHeadless + check build_project: runs-on: ubuntu-latest if: github.event_name == 'push' steps: - - uses: actions/checkout@v4 - - uses: gradle/wrapper-validation-action@v2 - - uses: actions/setup-java@v4 - with: { java-version: 17, distribution: temurin } - - name: Run Build - uses: gradle/actions/setup-gradle@v3 - env: - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} - GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} + - name: "ðŸ“Ĩ Checkout repository" + uses: actions/checkout@v4 + - name: "☕ïļ Setup JDK" + uses: actions/setup-java@v4 with: - arguments: build -Dgeb.env=chromeHeadless -x test -x integrationTest - - - name: Publish Snapshot artifacts to Artifactory (repo.grails.org) + java-version: 17 + distribution: temurin + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@v4 + with: + develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + - name: "ðŸ”Ļ Build project" + env: + DEVELOCITY_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} + DEVELOCITY_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} + run: > + ./gradlew + -Dgeb.env=chromeHeadless + build + - name: "ðŸ“Ī Publish Snapshot Artifacts to Artifactory (repo.grails.org)" if: success() - uses: gradle/actions/setup-gradle@v3 env: - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} - GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} + DEVELOVITY_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} + DEVELOCITY_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} ORG_GRADLE_PROJECT_artifactoryUsername: ${{ secrets.ARTIFACTORY_USERNAME }} ORG_GRADLE_PROJECT_artifactoryPassword: ${{ secrets.ARTIFACTORY_PASSWORD }} - with: - arguments: | - -Dorg.gradle.internal.publish.checksums.insecure=true - publish - - - name: Generate Snapshot Documentation + run: > + ./gradlew + -Dorg.gradle.internal.publish.checksums.insecure=true + publish + - name: "📖 Generate Snapshot Documentation" if: success() - uses: gradle/actions/setup-gradle@v3 env: - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} - GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} - with: - arguments: views-docs:docs - - - name: Publish Snapshot Documentation to Github Pages + DEVELOCITY_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} + DEVELOCITY_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} + run: > + ./gradlew + views-docs:docs + - name: "ðŸ“Ī Publish Snapshot Documentation to Github Pages" if: success() uses: micronaut-projects/github-pages-deploy-action@grails env: diff --git a/.github/workflows/groovy-joint-workflow.yml b/.github/workflows/groovy-joint-workflow.yml index 5a943537a..2fe97092e 100644 --- a/.github/workflows/groovy-joint-workflow.yml +++ b/.github/workflows/groovy-joint-workflow.yml @@ -1,22 +1,4 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "Grails Joint Validation Build" -# GROOVY_2_5_X == Grails 4.0.x -# GROOVY_3_0_X == grails master -# Groovy master branch does not map to any due to changed package names. +name: "Groovy Joint Validation Build" on: push: branches: @@ -27,138 +9,138 @@ on: workflow_dispatch: permissions: contents: read -env: - CI_GROOVY_VERSION: jobs: build_groovy: - strategy: - fail-fast: true runs-on: ubuntu-latest outputs: groovyVersion: ${{ steps.groovy-version.outputs.value }} steps: - - - name: Set up JDK + - name: "☕ïļ Setup JDK" uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 - - - name: Cache local Maven repository & Groovy + - name: "🗄ïļ Cache local Maven repository" uses: actions/cache@v4 with: - path: | - ~/groovy - ~/.m2/repository - key: cache-local-groovy-maven-${{ github.sha }} - - name: Checkout Groovy 4_0_X (Grails 7 and later) - run: cd .. && git clone --depth 1 https://github.com/apache/groovy.git -b GROOVY_4_0_X --single-branch - - name: Set CI_GROOVY_VERSION for Grails + path: ~/.m2/repository + key: cache-local-maven-${{ github.sha }} + - name: "ðŸ“Ĩ Checkout Grails Views to fetch Gradle Plugin versions it uses" + uses: actions/checkout@v4 + with: + sparse-checkout-cone-mode: false + sparse-checkout: settings.gradle + - name: "📝 Store the Gradle Plugin versions used in Grails Views" + id: gradle-plugin-versions + run: | + DEVELOCITY_PLUGIN_VERSION=$(grep -m 1 'id\s*\(\"com.gradle.develocity\"\|'"'com.gradle.develocity'"'\)\s*version' settings.gradle | sed -E "s/.*version[[:space:]]*['\"]?([0-9]+\.[0-9]+(\.[0-9]+)?)['\"]?.*/\1/" | tr -d [:space:]) + COMMON_CUSTOM_USER_DATA_PLUGIN_VERSION=$(grep -m 1 'id\s*\(\"com.gradle.common-custom-user-data-gradle-plugin\"\|'"'com.gradle.common-custom-user-data-gradle-plugin'"'\)\s*version' settings.gradle | sed -E "s/.*version[[:space:]]*['\"]?([0-9]+\.[0-9]+(\.[0-9]+)?)['\"]?.*/\1/" | tr -d [:space:]) + echo "Project uses Develocity Plugin version: $DEVELOCITY_PLUGIN_VERSION" + echo "Project uses Common Custom User Data Plugin version: $COMMON_CUSTOM_USER_DATA_PLUGIN_VERSION" + echo "develocity_plugin_version=$DEVELOCITY_PLUGIN_VERSION" >> $GITHUB_OUTPUT + echo "common_custom_user_data_plugin_version=$COMMON_CUSTOM_USER_DATA_PLUGIN_VERSION" >> $GITHUB_OUTPUT + rm settings.gradle + - name: "ðŸ“Ĩ Checkout Groovy 4_0_X (Grails 7 and later)" + run: git clone --depth 1 https://github.com/apache/groovy.git -b GROOVY_4_0_X --single-branch + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@v4 + with: + develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + - name: "📝 Store Groovy version to use when building Grails Views" id: groovy-version run: | - cd ../groovy - echo "CI_GROOVY_VERSION=$(cat gradle.properties | grep groovyVersion | cut -d\= -f2 | tr -d '[:space:]')" >> $GITHUB_ENV - echo "value=$(cat gradle.properties | grep groovyVersion | cut -d\= -f2 | tr -d '[:space:]')" >> $GITHUB_OUTPUT - - name: Prepare Develocity Setup 1 - id: develocity_conf_1 + cd groovy + GROOVY_VERSION=$(cat gradle.properties | grep groovyVersion | cut -d\= -f2 | tr -d '[:space:]') + echo "Groovy version $GROOVY_VERSION stored" + echo "value=$GROOVY_VERSION" >> $GITHUB_OUTPUT + - name: "🐘 Configure Gradle Plugins (Step 1/3)" + id: develocity-conf-1 run: | echo "VALUE<> $GITHUB_OUTPUT echo "plugins { " >> $GITHUB_OUTPUT - echo " id 'com.gradle.enterprise' version '3.15.1'" >> $GITHUB_OUTPUT - echo " id 'com.gradle.common-custom-user-data-gradle-plugin' version '1.11.3'" >> $GITHUB_OUTPUT + echo " id 'com.gradle.develocity' version '${{ steps.gradle-plugin-versions.outputs.develocity_plugin_version }}'" >> $GITHUB_OUTPUT + echo " id 'com.gradle.common-custom-user-data-gradle-plugin' version '${{ steps.gradle-plugin-versions.outputs.common_custom_user_data_plugin_version }}'" >> $GITHUB_OUTPUT echo "}" >> $GITHUB_OUTPUT echo "" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - - name: Prepare Develocity Setup 2 - id: develocity_conf_2 + - name: "🐘 Configure Gradle Plugins (Step 2/3)" + id: develocity-conf-2 run: | echo "VALUE<> $GITHUB_OUTPUT - echo "gradleEnterprise {" >> $GITHUB_OUTPUT - echo " server = 'https://ge.grails.org'" >> $GITHUB_OUTPUT - echo " buildScan {" >> $GITHUB_OUTPUT - echo " publishAlways()" >> $GITHUB_OUTPUT - echo " publishIfAuthenticated()" >> $GITHUB_OUTPUT - echo " uploadInBackground = System.getenv('CI') == null" >> $GITHUB_OUTPUT - echo " capture {" >> $GITHUB_OUTPUT - echo " taskInputFiles = true" >> $GITHUB_OUTPUT - echo " }" >> $GITHUB_OUTPUT - echo " }" >> $GITHUB_OUTPUT - echo "}" >> $GITHUB_OUTPUT + echo "def isAuthenticated = System.getenv('DEVELOCITY_ACCESS_KEY') != null" >> $GITHUB_OUTPUT + echo "def isBuildCacheAuthenticated =" >> $GITHUB_OUTPUT + echo " System.getenv('DEVELOCITY_BUILD_CACHE_NODE_USER') != null &&" >> $GITHUB_OUTPUT + echo " System.getenv('DEVELOCITY_BUILD_CACHE_NODE_KEY') != null" >> $GITHUB_OUTPUT echo "" >> $GITHUB_OUTPUT - echo "buildCache {" >> $GITHUB_OUTPUT - echo " local { enabled = System.getenv('CI') != 'true' }" >> $GITHUB_OUTPUT - echo " remote(HttpBuildCache) {" >> $GITHUB_OUTPUT - echo " push = System.getenv('CI') == 'true'" >> $GITHUB_OUTPUT - echo " enabled = true" >> $GITHUB_OUTPUT - echo " url = 'https://ge.grails.org/cache/'" >> $GITHUB_OUTPUT - echo " credentials {" >> $GITHUB_OUTPUT - echo " username = System.getenv('GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER')" >> $GITHUB_OUTPUT - echo " password = System.getenv('GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY')" >> $GITHUB_OUTPUT - echo " }" >> $GITHUB_OUTPUT - echo " }" >> $GITHUB_OUTPUT - echo "}" >> $GITHUB_OUTPUT + echo "develocity {" >> $GITHUB_OUTPUT + echo " server = 'https://ge.grails.org'" >> $GITHUB_OUTPUT + echo " buildScan {" >> $GITHUB_OUTPUT + echo " publishing.onlyIf { isAuthenticated }" >> $GITHUB_OUTPUT + echo " uploadInBackground = false" >> $GITHUB_OUTPUT + echo " }" >> $GITHUB_OUTPUT + echo "}" >> $GITHUB_OUTPUT + echo "" >> $GITHUB_OUTPUT + echo "buildCache {" >> $GITHUB_OUTPUT + echo " local { enabled = false }" >> $GITHUB_OUTPUT + echo " remote(develocity.buildCache) {" >> $GITHUB_OUTPUT + echo " push = isBuildCacheAuthenticated" >> $GITHUB_OUTPUT + echo " enabled = true" >> $GITHUB_OUTPUT + echo " usernameAndPassword(" >> $GITHUB_OUTPUT + echo " System.getenv('DEVELOCITY_BUILD_CACHE_NODE_USER') ?: ''," >> $GITHUB_OUTPUT + echo " System.getenv('DEVELOCITY_BUILD_CACHE_NODE_KEY') ?: ''" >> $GITHUB_OUTPUT + echo " )" >> $GITHUB_OUTPUT + echo " }" >> $GITHUB_OUTPUT + echo "}" >> $GITHUB_OUTPUT echo "" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - - name: Develocity Set-up + - name: "🐘 Configure Gradle Plugins (step 3/3)" run: | - cd ../groovy + cd groovy # Delete existing plugins from settings.gradle file sed -i '32,37d' settings.gradle - # Add Gradle Enterprise set-up related configuration after line no 31 in settings.gradle - echo "${{ steps.develocity_conf_1.outputs.value }}" | sed -i -e "31r /dev/stdin" settings.gradle + # Add Develocity setup related configuration after line no 31 in settings.gradle + echo "${{ steps.develocity-conf-1.outputs.value }}" | sed -i -e "31r /dev/stdin" settings.gradle # Delete existing buildCache configuration from gradle/build-scans.gradle file sed -i '23,46d' gradle/build-scans.gradle - # Add Gradle Enterprise set-up related configuration after line no 22 in gradle/build-scans.gradle - echo "${{ steps.develocity_conf_2.outputs.value }}" | sed -i -e "22r /dev/stdin" gradle/build-scans.gradle - - name: Build and install groovy (no docs) - uses: gradle/gradle-build-action@v3 + # Add Develocity setup related configuration after line no 22 in gradle/build-scans.gradle + echo "${{ steps.develocity-conf-2.outputs.value }}" | sed -i -e "22r /dev/stdin" gradle/build-scans.gradle + - name: "ðŸ”Ļ Publish Groovy to local maven repository (no docs)" env: - GRADLE_SCANS_ACCEPT: yes - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} - GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} - with: - build-root-directory: ../groovy - arguments: | - install - -x groovydoc - -x javadoc - -x javadocAll - -x groovydocAll - -x asciidoc - -x docGDK + DEVELOCITY_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} + DEVELOCITY_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} + run: | + cd groovy + ./gradlew pTML -x groovydoc -x javadoc -x javadocAll -x groovydocAll -x asciidoc -x docGDK build_project: needs: [build_groovy] - strategy: - fail-fast: true runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Set up JDK + - name: "ðŸ“Ĩ Checkout project" + uses: actions/checkout@v4 + - name: "☕ïļ Setup JDK" uses: actions/setup-java@v4 with: - distribution: temurin java-version: 17 - - name: Cache local Maven repository & Groovy + distribution: temurin + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@v4 + with: + develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + - name: "🗄ïļ Restore local Maven repository from cache" uses: actions/cache@v4 with: - path: | - ~/groovy - ~/.m2/repository - key: cache-local-groovy-maven-${{ github.sha }} - - - name: Build Project - uses: gradle/actions/setup-gradle@v3 + path: ~/.m2/repository + key: cache-local-maven-${{ github.sha }} + - name: "ðŸŠķ Add mavenLocal repository to build" + run: sed -i 's|// mavenLocal() // Keep|mavenLocal() // Keep|' build.gradle + - name: "ðŸ”Ļ Build and test Grails using the locally built Groovy snapshot" env: - GROOVY_VERSION: ${{ needs.build_groovy.outputs.groovySnapshotVersion }} - CI_GROOVY_VERSION: ${{ needs.build_groovy.outputs.groovySnapshotVersion }} - ORG_GRADLE_PROJECT_groovyVersion: ${{ needs.build_groovy.outputs.groovySnapshotVersion }} - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} - GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} - with: - arguments: | - build - -x groovydoc - -Dgeb.env=chromeHeadless \ No newline at end of file + DEVELOCITY_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} + DEVELOCITY_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} + run: > + ./gradlew + build + -Dgeb.env=chromeHeadless + -PgroovyVersion=${{needs.build_groovy.outputs.groovyVersion}} + -x groovydoc \ No newline at end of file diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml index 8453cc17b..8eddb3cc0 100644 --- a/.github/workflows/release-notes.yml +++ b/.github/workflows/release-notes.yml @@ -1,4 +1,4 @@ -name: Changelog +name: "Release Drafter" on: issues: types: [closed,reopened] @@ -12,41 +12,12 @@ on: types: [opened, reopened, synchronize] workflow_dispatch: jobs: - release_notes: + update_release_draft: + permissions: + contents: read # limit to read access runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Check if it has release drafter config file - id: check_release_drafter - run: | - has_release_drafter=$([ -f .github/release-drafter.yml ] && echo "true" || echo "false") - echo "has_release_drafter=${has_release_drafter}" >> $GITHUB_OUTPUT - - name: Extract branch name - id: extract_branch - run: echo "value=${GITHUB_REF:11}" >> $GITHUB_OUTPUT - # If it has release drafter: - - uses: release-drafter/release-drafter@v6 - if: steps.check_release_drafter.outputs.has_release_drafter == 'true' + - name: "📝 Update Release Draft" + uses: release-drafter/release-drafter@v6 env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - with: - commitish: ${{ steps.extract_branch.outputs.value }} - # Otherwise: - - name: Export Gradle Properties - if: steps.check_release_drafter.outputs.has_release_drafter == 'false' - uses: micronaut-projects/github-actions/export-gradle-properties@master - - uses: micronaut-projects/github-actions/release-notes@master - if: steps.check_release_drafter.outputs.has_release_drafter == 'false' - id: release_notes - with: - token: ${{ secrets.GH_TOKEN }} - - uses: ncipollo/release-action@v1 - if: steps.check_release_drafter.outputs.has_release_drafter == 'false' && steps.release_notes.outputs.generated_changelog == 'true' - with: - allowUpdates: true - commit: ${{ steps.release_notes.outputs.current_branch }} - draft: true - name: ${{ env.title }} ${{ steps.release_notes.outputs.next_version }} - tag: v${{ steps.release_notes.outputs.next_version }} - bodyFile: CHANGELOG.md - token: ${{ secrets.GH_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 31f4a827a..9fbe3ec49 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,123 +1,130 @@ -name: Release +name: "Release" on: release: types: [published] +env: + GIT_USER_NAME: 'grails-build' + GIT_USER_EMAIL: 'grails-build@users.noreply.github.com' jobs: publish: outputs: release_version: ${{ steps.release_version.outputs.value }} runs-on: ubuntu-latest - env: - GIT_USER_NAME: 'grails-build' - GIT_USER_EMAIL: 'grails-build@users.noreply.github.com' steps: - - uses: actions/checkout@v4 - - uses: gradle/wrapper-validation-action@v2 - - uses: actions/setup-java@v4 + - name: "ðŸ“Ĩ Checkout repository" + uses: actions/checkout@v4 + - name: "☕ïļ Setup JDK" + uses: actions/setup-java@v4 with: java-version: 17 distribution: temurin - - name: Get the current release version + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@v4 + with: + develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + - name: "📝 Store the current release version" id: release_version run: echo "value=${GITHUB_REF:11}" >> $GITHUB_OUTPUT - - name: Set projectVersion to the release version + - name: "⚙ Run pre-release" uses: micronaut-projects/github-actions/pre-release@master - - name: Run Assemble + - name: "ðŸ§Đ Run Assemble" if: success() id: assemble - uses: gradle/gradle-build-action@v3 - with: - arguments: assemble env: - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} - GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} - - name: Upload Distribution + DEVELOCITY_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} + DEVELOCITY_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} + run: ./gradlew assemble + - name: "ðŸ“Ī Upload Distribution" if: success() uses: actions/upload-artifact@v4 with: name: grails-views-${{ steps.release_version.outputs.value }} path: ./**/build/libs/* - - name: Generate key file for artifact signing + - name: "🔐 Generate key file for artifact signing" env: SECRING_FILE: ${{ secrets.SECRING_FILE }} run: echo $SECRING_FILE | base64 -d > ${{ github.workspace }}/secring.gpg - - name: Publish release artifacts to Sonatype - id: publish_to_sonatype - uses: gradle/actions/setup-gradle@v3 + - name: "ðŸ“Ī Publish release artifacts to Sonatype" env: - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} - GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} + DEVELOCITY_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} + DEVELOCITY_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} ORG_GRADLE_PROJECT_sonatypeOssUsername: ${{ secrets.SONATYPE_USERNAME }} ORG_GRADLE_PROJECT_sonatypeOssPassword: ${{ secrets.SONATYPE_PASSWORD }} ORG_GRADLE_PROJECT_sonatypeOssStagingProfileId: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} ORG_GRADLE_PROJECT_sonatypeOssRepo: ${{ secrets.SONATYPE_NEXUS_URL }} SIGNING_KEY: ${{ secrets.SIGNING_KEY }} SIGNING_PASSPHRASE: ${{ secrets.SIGNING_PASSPHRASE }} - with: - arguments: | - -Psigning.secretKeyRingFile=${{ github.workspace }}/secring.gpg - publishToSonatype - closeSonatypeStagingRepository + run: > + ./gradlew + -Psigning.secretKeyRingFile=${{ github.workspace }}/secring.gpg + publishToSonatype + closeSonatypeStagingRepository release: needs: publish runs-on: ubuntu-latest permissions: - contents: read + contents: read # limit to read access steps: - - name: Checkout repository + - name: "ðŸ“Ĩ Checkout repository" uses: actions/checkout@v4 with: token: ${{ secrets.GH_TOKEN }} ref: v${{ needs.publish.outputs.release_version }} - - name: Nexus Staging Close And Release - uses: gradle/gradle-build-action@v3 + - name: "☕ïļ Setup JDK" + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: temurin + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@v4 + with: + develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + - name: "🚊 Nexus Staging Close And Release" env: - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} - GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} + DEVELOCITY_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} + DEVELOCITY_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} ORG_GRADLE_PROJECT_sonatypeOssUsername: ${{ secrets.SONATYPE_USERNAME }} ORG_GRADLE_PROJECT_sonatypeOssPassword: ${{ secrets.SONATYPE_PASSWORD }} ORG_GRADLE_PROJECT_sonatypeOssStagingProfileId: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} ORG_GRADLE_PROJECT_sonatypeOssRepo: ${{ secrets.SONATYPE_NEXUS_URL }} - with: - arguments: | - findSonatypeStagingRepository - releaseSonatypeStagingRepository - - name: Run post-release + run: > + ./gradlew + findSonatypeStagingRepository + releaseSonatypeStagingRepository + - name: "⚙ïļ Run post-release" if: success() uses: micronaut-projects/github-actions/post-release@master with: token: ${{ secrets.GITHUB_TOKEN }} - env: - SNAPSHOT_SUFFIX: -SNAPSHOT docs: needs: publish runs-on: ubuntu-latest permissions: contents: write steps: - - name: Checkout repository + - name: "ðŸ“Ĩ Checkout repository" uses: actions/checkout@v4 with: token: ${{ secrets.GH_TOKEN }} ref: v${{ needs.publish.outputs.release_version }} - - name: Set up JDK + - name: "☕ïļ Setup JDK" uses: actions/setup-java@v4 with: - distribution: 'temurin' - java-version: '17' - - name: Generate Documentation + java-version: 17 + distribution: temurin + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@v4 + with: + develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + - name: "📖 Generate Documentation" if: success() - uses: gradle/actions/setup-gradle@v3 env: - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} - GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} - with: - arguments: views-docs:docs - - name: Publish Documentation to Github Pages + DEVELOCITY_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} + DEVELOCITY_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} + run: > + ./gradlew + views-docs:docs + - name: "ðŸ“Ī Publish Documentation to Github Pages" if: success() uses: micronaut-projects/github-pages-deploy-action@grails env: diff --git a/build.gradle b/build.gradle index bf85877e8..fb53c3442 100644 --- a/build.gradle +++ b/build.gradle @@ -46,9 +46,9 @@ allprojects { version = rootProject.version repositories { - mavenLocal() // Used by Groovy Joint workflow github action after building Groovy mavenCentral() maven { url = 'https://repo.grails.org/grails/core' } + // mavenLocal() // Keep, this will be uncommented and used by CI (groovy-joint-workflow) if (libs.versions.groovy.get().endsWith('-SNAPSHOT')) { maven { name = 'JFrog Groovy snapshot repo' diff --git a/examples/functional-tests-plugin/build.gradle b/examples/functional-tests-plugin/build.gradle index 9eb70e048..a8980af4b 100644 --- a/examples/functional-tests-plugin/build.gradle +++ b/examples/functional-tests-plugin/build.gradle @@ -1,7 +1,7 @@ plugins { id 'java-library' id 'org.grails.grails-plugin' - //id 'org.grails.plugins.views-json' + id 'org.grails.plugins.views-json' } group = 'functional.tests.plugin' diff --git a/examples/functional-tests/build.gradle b/examples/functional-tests/build.gradle index c60a96bf4..640331a58 100644 --- a/examples/functional-tests/build.gradle +++ b/examples/functional-tests/build.gradle @@ -10,15 +10,10 @@ group = 'functional.tests' dependencies { - implementation project(':examples-functional-tests-plugin') implementation project(':views-json') implementation project(':views-markup') - runtimeOnly project(':views-json-templates') - - testImplementation project(':views-json-testing-support') - implementation 'org.grails:grails-core' implementation 'org.grails:grails-logging' implementation 'org.grails:grails-web-boot' @@ -38,14 +33,20 @@ dependencies { implementation 'org.springframework.boot:spring-boot-autoconfigure' implementation 'org.springframework.boot:spring-boot-starter-logging' implementation 'org.springframework.boot:spring-boot-starter-tomcat' - implementation libs.jakarta.servlet.api + compileOnly libs.jakarta.servlet.api // Provided by Tomcat + + runtimeOnly project(':views-json-templates') runtimeOnly 'com.h2database:h2' runtimeOnly 'org.apache.tomcat:tomcat-jdbc' runtimeOnly libs.assetpipeline + testImplementation project(':views-json-testing-support') testImplementation libs.grails.testing.support.core - testImplementation libs.micronaut.http.client + + integrationTestImplementation libs.jackson.databind + integrationTestImplementation libs.micronaut.http.client + integrationTestImplementation libs.micronaut.jackson.databind } assets { @@ -56,11 +57,3 @@ assets { java { sourceCompatibility = JavaVersion.toVersion(libs.versions.java.baseline.get()) } - -distTar { - duplicatesStrategy = DuplicatesStrategy.INCLUDE -} - -distZip { - duplicatesStrategy = DuplicatesStrategy.INCLUDE -} \ No newline at end of file diff --git a/examples/functional-tests/grails-app/controllers/functional/tests/BookController.groovy b/examples/functional-tests/grails-app/controllers/functional/tests/BookController.groovy index 5b0273d8f..169a66fa5 100644 --- a/examples/functional-tests/grails-app/controllers/functional/tests/BookController.groovy +++ b/examples/functional-tests/grails-app/controllers/functional/tests/BookController.groovy @@ -16,7 +16,6 @@ class BookController extends RestfulController { [books: listAllResources(params)] } - def listExcludesRespond() { respond([books: listAllResources(params)]) } @@ -34,7 +33,7 @@ class BookController extends RestfulController { } def nonStandardTemplate() { - respond([book: new Book(title: 'template found'), custom: new CustomClass(name: "Sally")], view:'/non-standard/template') + respond([book: new Book(title: 'template found'), custom: new CustomClass(name: 'Sally')], view:'/non-standard/template') } def showWithParams() { diff --git a/examples/functional-tests/grails-app/domain/functional/tests/Garage.groovy b/examples/functional-tests/grails-app/domain/functional/tests/Garage.groovy index f03dac0b7..f908ce8ac 100644 --- a/examples/functional-tests/grails-app/domain/functional/tests/Garage.groovy +++ b/examples/functional-tests/grails-app/domain/functional/tests/Garage.groovy @@ -4,6 +4,7 @@ class Garage { String owner - static hasMany = [vehicles: Vehicle] + // GORM Inheritance not working in Groovy 4 + //static hasMany = [vehicles: Vehicle] } diff --git a/examples/functional-tests/grails-app/init/BootStrap.groovy b/examples/functional-tests/grails-app/init/BootStrap.groovy index a13e4aedb..f6206cd39 100644 --- a/examples/functional-tests/grails-app/init/BootStrap.groovy +++ b/examples/functional-tests/grails-app/init/BootStrap.groovy @@ -36,10 +36,15 @@ class BootStrap { new Proxy(name: "Sally").save(flush: true, failOnError: true) + // GORM inheritance not working in Groovy 4 + // See https://issues.apache.org/jira/browse/GROOVY-5106, + // https://github.com/grails/grails-views/pull/589 + /* new Garage(owner: "Jay Leno") .addToVehicles(new Bus(maxPassengers: 30, route: "around town")) .addToVehicles(new Car(maxPassengers: 4, make: "Subaru", model: "WRX", year: 2016)) .save(flush: true, failOnError: true) + */ new Customer(name: "Nokia") .addToSites(new Site(name: "Salo")) diff --git a/examples/functional-tests/src/integration-test/groovy/functional/tests/BookSpec.groovy b/examples/functional-tests/src/integration-test/groovy/functional/tests/BookSpec.groovy index 3c7465766..fa8af0a03 100644 --- a/examples/functional-tests/src/integration-test/groovy/functional/tests/BookSpec.groovy +++ b/examples/functional-tests/src/integration-test/groovy/functional/tests/BookSpec.groovy @@ -1,5 +1,6 @@ package functional.tests +import com.fasterxml.jackson.databind.ObjectMapper import grails.testing.mixin.integration.Integration import grails.testing.spock.RunOnce import grails.web.http.HttpHeaders @@ -9,169 +10,239 @@ import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus import io.micronaut.http.client.exceptions.HttpClientResponseException import org.junit.jupiter.api.BeforeEach +import spock.lang.Shared @Integration(applicationClass = Application) class BookSpec extends HttpClientSpec { + @Shared + ObjectMapper objectMapper + + def setupSpec() { + objectMapper = new ObjectMapper() + } + @RunOnce @BeforeEach void init() { super.init() } - void "Test errors view rendering"() { - when:"A POST is issued" - HttpRequest request = HttpRequest.POST("/books", [title: ""]) - HttpResponse resp = client.toBlocking().exchange(request, Argument.of(String), Argument.of(String)) + void 'Test errors view rendering'() { + when: 'A POST is issued with a missing title' + HttpRequest request = HttpRequest.POST('/books', [title: '']) + client.toBlocking().exchange(request, Argument.of(String), Argument.of(String)) - then:"The REST resource is created and the correct JSON is returned" + then: 'The proper error is returned' HttpClientResponseException e = thrown() e.response.status == HttpStatus.UNPROCESSABLE_ENTITY e.response.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() - e.response.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/vnd.error;charset=UTF-8' - e.response.body().contains('{"message":"Property [title] of class [class functional.tests.Book] cannot be null","path":"/book/index","_links":{"self":{"href":"'+baseUrl+'/book/index"}}}') + // This has changed somewhere along the way + // e.response.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/vnd.error;charset=UTF-8' + // to -> + e.response.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' + objectMapper.readTree(e.response.body().toString()) == objectMapper.readTree(''' + { + "errors": [ + { + "object": "functional.tests.Book", + "field": "title", + "rejected-value": null, + "message": "Property [title] of class [class functional.tests.Book] cannot be null" + } + ] + } + ''') } - void "test REST view rendering"() { - when: - HttpRequest request = HttpRequest.GET("/books") - HttpResponse resp = client.toBlocking().exchange(request, String) + void 'Test REST view rendering'() { + when: 'A GET is issued to get all books' + HttpRequest request = HttpRequest.GET('/books') + def resp = client.toBlocking().exchange(request, String) - then:"The response is correct" + then: 'The response is correct' resp.status == HttpStatus.OK resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' resp.body() == '[]' - when:"A POST is issued" - resp = client.toBlocking().exchange(HttpRequest.POST("/books", new SaveBookVM(title: "The Stand")), Map) + when: 'A POST is issued to create a new book' + request = HttpRequest.POST('/books', new SaveBookVM(title: 'The Stand')) + resp = client.toBlocking().exchange(request, Map) - then:"The REST resource is created and the correct JSON is returned" + then: 'The REST resource is created and the correct JSON is returned' resp.status == HttpStatus.CREATED resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' resp.body() resp.body().id == 1 - resp.body().timeZone == "America/New_York" - resp.body().title == "The Stand" - resp.body().vendor == "MyCompany" + resp.body().timeZone == 'America/New_York' + resp.body().title == 'The Stand' + resp.body().vendor == 'MyCompany' - when:"A GET request is issued" + when: 'A GET request is issued' request = HttpRequest.GET("/books/${resp.body().id}") resp = client.toBlocking().exchange(request, Map) - then:"The response is correct" + then: 'The response is correct' resp.status == HttpStatus.OK resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' resp.body() resp.body().id == 1 - resp.body().timeZone == "America/New_York" - resp.body().title == "The Stand" - resp.body().vendor == "MyCompany" + resp.body().timeZone == 'America/New_York' + resp.body().title == 'The Stand' + resp.body().vendor == 'MyCompany' - when:"A PUT is issued" - resp = client.toBlocking().exchange(HttpRequest.PUT("/books/${resp.body().id}", new SaveBookVM(title: "The Changeling")), Map) + when: 'A PUT is issued' + resp = client.toBlocking().exchange(HttpRequest.PUT("/books/${resp.body().id}", new SaveBookVM(title: 'The Changeling')), Map) - then:"The resource is updated" + then: 'The resource is updated' resp.status == HttpStatus.OK resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' resp.body() resp.body().id == 1 - resp.body().timeZone == "America/New_York" - resp.body().title == "The Changeling" - resp.body().vendor == "MyCompany" + resp.body().timeZone == 'America/New_York' + resp.body().title == 'The Changeling' + resp.body().vendor == 'MyCompany' - when:"A GET is issued for all books" - request = HttpRequest.GET("/books") + when: 'A GET is issued for all books' + request = HttpRequest.GET('/books') resp = client.toBlocking().exchange(request, String) - then:"The response is correct" + then: 'The response is correct' resp.status == HttpStatus.OK resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' - resp.body() == '[{"id":1,"title":"The Changeling","timeZone":"America/New_York","vendor":"MyCompany"}]' - - when:"A GET is issued for all books with excludes" - request = HttpRequest.GET("/books/listExcludes?testParam=3") + objectMapper.readTree(resp.body()) == objectMapper.readTree(''' + [ + { + "id": 1, + "title": "The Changeling", + "timeZone": "America/New_York", + "vendor": "MyCompany" + } + ] + ''') + + when: 'A GET is issued for all books with excludes' + request = HttpRequest.GET('/books/listExcludes?testParam=3') resp = client.toBlocking().exchange(request, String) - then:"Access to config and params works" + then: 'Access to config and params works' resp.status == HttpStatus.OK resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' - resp.body() == '[{"id":1,"timeZone":"America/New_York","title":"The Changeling","vendor":"ConfigVendor","fromParams":3}]' - - when:"A GET is issued for all books with excludes" - request = HttpRequest.GET("/books/listExcludesRespond?testParam=4") + objectMapper.readTree(resp.body()) == objectMapper.readTree(''' + [ + { + "id": 1, + "timeZone": "America/New_York", + "title": "The Changeling", + "vendor": "ConfigVendor", + "fromParams": 3 + } + ] + ''') + + when: 'A GET is issued for all books with excludes' + request = HttpRequest.GET('/books/listExcludesRespond?testParam=4') resp = client.toBlocking().exchange(request, String) - then:"view rendering works with a map with respond" + then: 'view rendering works with a map with respond' resp.status == HttpStatus.OK resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' - resp.body() == '[{"id":1,"timeZone":"America/New_York","vendor":"ConfigVendor","fromParams":4}]' - - when:"A GET is issued for a specific book rendered by a template" - request = HttpRequest.GET("/books/showWithParams/1?expand=foo") + objectMapper.readTree(resp.body()) == objectMapper.readTree(''' + [ + { + "id": 1, + "timeZone": "America/New_York", + "vendor": "ConfigVendor", + "fromParams": 4 + } + ] + ''') + + when: 'A GET is issued for a specific book rendered by a template' + request = HttpRequest.GET('/books/showWithParams/1?expand=foo') resp = client.toBlocking().exchange(request, Map) - then:"view rendering with template passes parameters" + then: 'view rendering with template passes parameters' resp.status == HttpStatus.OK resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' - resp.body().paramsFromView == resp.body().book.paramsFromTemplate + resp.body().paramsFromView == resp.body().book['paramsFromTemplate'] } - void "View parameter passed to the render method can be used for non-standard view locations"() { - when:"A GET is issued to a request with a template at a non-standard location" - HttpRequest request = HttpRequest.GET("/books/non-standard-template") + void 'View parameter passed to the render method can be used for non-standard view locations'() { + when: 'A GET is issued to a request with a template at a non-standard location' + HttpRequest request = HttpRequest.GET('/books/non-standard-template') HttpResponse resp = client.toBlocking().exchange(request, String) - then:"The template was rendered successfully. The custom converter was also used" + then: 'The template was rendered successfully. The custom converter was also used' resp.status == HttpStatus.OK resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' - resp.body() == '{"bookTitle":"template found","custom":"Sally"}' + objectMapper.readTree(resp.body()) == objectMapper.readTree(''' + { + "bookTitle": "template found", + "custom": "Sally" + } + ''') } - void "Object type of list is used for model variable when rendering templates"() { + void 'Object type of list is used for model variable when rendering templates'() { when: - HttpRequest request = HttpRequest.GET("/books/listCallsTmpl") + HttpRequest request = HttpRequest.GET('/books/listCallsTmpl') HttpResponse resp = client.toBlocking().exchange(request, String) - then:"The template was rendered successfully" + then: 'The template was rendered successfully' resp.status == HttpStatus.OK resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' resp.body() == '[{"title":"The Changeling"}]' } - void "Object type of list is used for model variable in addition to specified model when rendering templates"() { + void 'Object type of list is used for model variable in addition to specified model when rendering templates'() { when: HttpRequest request = HttpRequest.GET("/books/listCallsTmplExtraData") HttpResponse resp = client.toBlocking().exchange(request, String) - then:"The template was rendered successfully" + then: 'The template was rendered successfully' resp.status == HttpStatus.OK resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' - resp.body() == '[{"title":"The Changeling","value":true}]' + objectMapper.readTree(resp.body()) == objectMapper.readTree(''' + [ + { + "title": "The Changeling", + "value": true + } + ] + ''') } - void "Object type of list is used for model variable in addition to specified model and var when rendering templates"() { + void 'Object type of list is used for model variable in addition to specified model and var when rendering templates'() { when: - HttpRequest request = HttpRequest.GET("/books/listCallsTmplVar") + HttpRequest request = HttpRequest.GET('/books/listCallsTmplVar') HttpResponse resp = client.toBlocking().exchange(request, String) - then:"The template was rendered successfully" + then: 'The template was rendered successfully' resp.status == HttpStatus.OK resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' - resp.body() == '[{"title":"The Changeling","value":true}]' + objectMapper.readTree(resp.body()) == objectMapper.readTree(''' + [ + { + "title": "The Changeling", + "value": true + } + ] + ''') } } diff --git a/examples/functional-tests/src/integration-test/groovy/functional/tests/EmbeddedSpec.groovy b/examples/functional-tests/src/integration-test/groovy/functional/tests/EmbeddedSpec.groovy index 151b44283..de82d448f 100644 --- a/examples/functional-tests/src/integration-test/groovy/functional/tests/EmbeddedSpec.groovy +++ b/examples/functional-tests/src/integration-test/groovy/functional/tests/EmbeddedSpec.groovy @@ -1,5 +1,6 @@ package functional.tests +import com.fasterxml.jackson.databind.ObjectMapper import grails.testing.mixin.integration.Integration import grails.testing.spock.RunOnce import io.micronaut.http.HttpRequest @@ -7,43 +8,81 @@ import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus import org.junit.jupiter.api.BeforeEach import spock.lang.Issue +import spock.lang.Shared import static io.micronaut.http.HttpHeaders.CONTENT_TYPE @Integration class EmbeddedSpec extends HttpClientSpec { + @Shared + ObjectMapper objectMapper + + def setup() { + objectMapper = new ObjectMapper() + } + @RunOnce @BeforeEach void init() { super.init() } - void "Test render can handle a domain with an embedded src/groovy class"() { + void 'Test render can handle a domain with an embedded src/groovy class'() { when: HttpRequest request = HttpRequest.GET('/embedded') HttpResponse rsp = client.toBlocking().exchange(request, String) - then:"The response is correct" + then: 'The response is correct' rsp.status() == HttpStatus.OK rsp.getHeaders().get(CONTENT_TYPE) == 'application/json;charset=UTF-8' - rsp.body() == '{"id":1,"customClass":{"name":"Bar"},"name":"Foo","inSameFile":{"text":"FooBar"}}' + objectMapper.readTree(rsp.body()) == objectMapper.readTree(''' + { + "id": 1, + "customClass": { + "name": "Bar" + }, + "name": "Foo", + "inSameFile": { + "text": "FooBar" + } + } + ''') } - void "Test jsonapi render can handle a domain with an embedded src/groovy class"() { + void 'Test jsonapi render can handle a domain with an embedded src/groovy class'() { when: HttpRequest request = HttpRequest.GET('/embedded/jsonapi') HttpResponse rsp = client.toBlocking().exchange(request, String) - then:"The response is correct" + then: 'The response is correct' rsp.status() == HttpStatus.OK rsp.getHeaders().get(CONTENT_TYPE) == 'application/json;charset=UTF-8' - rsp.body() == '{"data":{"type":"embedded","id":"2","attributes":{"customClass":{"name":"Bar2"},"name":"Foo2","inSameFile":{"text":"FooBar2"}}},"links":{"self":"/embedded/show/2"}}' + objectMapper.readTree(rsp.body()) == objectMapper.readTree(''' + { + "data": { + "type": "embedded", + "id": "2", + "attributes": { + "customClass": { + "name": "Bar2" + }, + "name": "Foo2", + "inSameFile": { + "text": "FooBar2" + } + } + }, + "links": { + "self": "/embedded/show/2" + } + } + ''') } - @Issue("https://github.com/grails/grails-views/issues/171") + @Issue('https://github.com/grails/grails-views/issues/171') void 'test render can handle a domain with an embedded and includes src/groovy class'() { when: HttpRequest request = HttpRequest.GET('/embedded/embeddedWithIncludes') @@ -52,10 +91,17 @@ class EmbeddedSpec extends HttpClientSpec { then: 'the response is correct' rsp.status() == HttpStatus.OK rsp.getHeaders().get(CONTENT_TYPE) == 'application/json;charset=UTF-8' - rsp.body() == '{"customClass":{"name":"Bar3"},"name":"Foo3"}' + objectMapper.readTree(rsp.body()) == objectMapper.readTree(''' + { + "customClass": { + "name": "Bar3" + }, + "name": "Foo3" + } + ''') } - @Issue("https://github.com/grails/grails-views/issues/171") + @Issue('https://github.com/grails/grails-views/issues/171') void 'Test jsonapi render can handle a domain with an embedded and includes src/groovy class'() { when: HttpRequest request = HttpRequest.GET('/embedded/embeddedWithIncludesJsonapi') @@ -64,6 +110,22 @@ class EmbeddedSpec extends HttpClientSpec { then: 'the response is correct' rsp.status() == HttpStatus.OK rsp.getHeaders().get(CONTENT_TYPE) == 'application/json;charset=UTF-8' - rsp.body() == '{"data":{"type":"embedded","id":"4","attributes":{"customClass":{"name":"Bar4"},"name":"Foo4"}},"links":{"self":"/embedded/show/4"}}' + objectMapper.readTree(rsp.body()) == objectMapper.readTree(''' + { + "data": { + "type": "embedded", + "id": "4", + "attributes": { + "customClass": { + "name": "Bar4" + }, + "name": "Foo4" + } + }, + "links": { + "self": "/embedded/show/4" + } + } + ''') } -} +} \ No newline at end of file diff --git a/examples/functional-tests/src/integration-test/groovy/functional/tests/HttpClientSpec.groovy b/examples/functional-tests/src/integration-test/groovy/functional/tests/HttpClientSpec.groovy index 7f3933ee5..d3bb8f020 100644 --- a/examples/functional-tests/src/integration-test/groovy/functional/tests/HttpClientSpec.groovy +++ b/examples/functional-tests/src/integration-test/groovy/functional/tests/HttpClientSpec.groovy @@ -1,6 +1,5 @@ package functional.tests - import grails.testing.spock.RunOnce import io.micronaut.http.client.HttpClient import org.junit.jupiter.api.BeforeEach diff --git a/examples/functional-tests/src/integration-test/groovy/functional/tests/ProductSpec.groovy b/examples/functional-tests/src/integration-test/groovy/functional/tests/ProductSpec.groovy index c0e276a46..84a27feb4 100644 --- a/examples/functional-tests/src/integration-test/groovy/functional/tests/ProductSpec.groovy +++ b/examples/functional-tests/src/integration-test/groovy/functional/tests/ProductSpec.groovy @@ -1,5 +1,6 @@ package functional.tests +import com.fasterxml.jackson.databind.ObjectMapper import grails.testing.mixin.integration.Integration import grails.testing.spock.RunOnce import grails.web.http.HttpHeaders @@ -7,10 +8,18 @@ import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus import org.junit.jupiter.api.BeforeEach +import spock.lang.Shared @Integration(applicationClass = Application) class ProductSpec extends HttpClientSpec { + @Shared + ObjectMapper objectMapper + + def setup() { + objectMapper = new ObjectMapper() + } + @RunOnce @BeforeEach void init() { @@ -19,102 +28,117 @@ class ProductSpec extends HttpClientSpec { void testEmptyProducts() { when: - HttpRequest request = HttpRequest.GET("/products") - HttpResponse resp = client.toBlocking().exchange(request, Map) + HttpRequest request = HttpRequest.GET('/products') + HttpResponse resp = client.toBlocking().exchange(request, String) + Map body = objectMapper.readValue(resp.body(), Map) then: resp.status == HttpStatus.OK resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/hal+json;charset=UTF-8' - and: "The values returned are there" - resp.body().count == 0 - resp.body().max == 10 - resp.body().offset == 0 - resp.body().sort == null - resp.body().order == null - and: "the hal _links attribute is present" - resp.body()._links.size() == 1 - resp.body()._links.self.href.startsWith("${baseUrl}/product") - - and: "there are no products yet" - resp.body()._embedded.products.size() == 0 + and: 'The values returned are there' + body.count == 0 + body.max == 10 + body.offset == 0 + body.sort == null + body.order == null + + and: 'the hal _links attribute is present' + body._links.size() == 1 + body._links.self.href.startsWith("${baseUrl}/product") + + and: 'there are no products yet' + body._embedded.products.size() == 0 } void testSingleProduct() { given: - HttpResponse createResp = client.toBlocking() - .exchange( - HttpRequest.POST("/products", [name: "Product 1", - description: "product 1 description", - price: 123.45]), Map) - assert createResp.status == HttpStatus.CREATED + HttpRequest request = HttpRequest.POST('/products', [ + name: 'Product 1', + description: 'product 1 description', + price: 123.45 + ]) - when: "We get the products" - HttpRequest request = HttpRequest.GET("/products") - HttpResponse resp = client.toBlocking().exchange(request, Map) + when: + HttpResponse createResp = client.toBlocking().exchange(request, String) + Map createBody = objectMapper.readValue(createResp.body(), Map) + + then: + createResp.status == HttpStatus.CREATED + + when: 'We get the products' + request = HttpRequest.GET('/products') + HttpResponse resp = client.toBlocking().exchange(request, String) + Map body = objectMapper.readValue(resp.body(), Map) then: resp.status == HttpStatus.OK resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/hal+json;charset=UTF-8' - and: "The values returned are there" - resp.body().count == 1 - resp.body().max == 10 - resp.body().offset == 0 - resp.body().sort == null - resp.body().order == null - and: "the hal _links attribute is present" - resp.body()._links.size() == 1 - resp.body()._links.self.href.startsWith("${baseUrl}/product") + and: 'The values returned are there' + body.count == 1 + body.max == 10 + body.offset == 0 + body.sort == null + body.order == null + + and: 'the hal _links attribute is present' + body._links.size() == 1 + body._links.self.href.startsWith("${baseUrl}/product") - and: "the product is present" - resp.body()._embedded.products.size() == 1 - resp.body()._embedded.products.first().name == "Product 1" + and: 'the product is present' + body._embedded.products.size() == 1 + body._embedded.products.first().name == 'Product 1' cleanup: - resp = client.toBlocking().exchange(HttpRequest.DELETE("/products/${createResp.body().id}")) + resp = client.toBlocking().exchange(HttpRequest.DELETE("/products/${createBody.id}")) assert resp.status() == HttpStatus.OK } - void "test a page worth of products"() { + void 'test a page worth of products'() { given: def productsIds = [] 15.times { productNumber -> - ProductVM product = new ProductVM(name: "Product $productNumber", - description: "product ${productNumber} description", - price: productNumber + (productNumber / 100)) - HttpResponse createResp = client.toBlocking() - .exchange(HttpRequest.POST("/products", product), Map) + ProductVM product = new ProductVM( + name: "Product $productNumber", + description: "product ${productNumber} description", + price: productNumber + (productNumber / 100) + ) + HttpResponse createResp = client.toBlocking() + .exchange(HttpRequest.POST('/products', product), String) + Map createBody = objectMapper.readValue(createResp.body(), Map) assert createResp.status == HttpStatus.CREATED - productsIds << createResp.body().id + productsIds << createBody.id } - when: "We get the products" - HttpRequest request = HttpRequest.GET("/products") - HttpResponse resp = client.toBlocking().exchange(request, Map) - def json = resp.body() + when: 'We get the products' + HttpRequest request = HttpRequest.GET('/products') + HttpResponse resp = client.toBlocking().exchange(request, String) + Map body = objectMapper.readValue(resp.body(), Map) + then: resp.status == HttpStatus.OK resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/hal+json;charset=UTF-8' - and: "The values returned are there" - json.count == 15 - json.max == 10 - json.offset == 0 - json.sort == null - json.order == null - and: "the hal _links attribute is present" - json._links.size() == 4 - json._links.self.href.startsWith("${baseUrl}/product") - json._links.first.href.startsWith("${baseUrl}/product") - json._links.next.href.startsWith("${baseUrl}/product") - json._links.last.href.startsWith("${baseUrl}/product") - - and: "the product is present" - json._embedded.products.size() == 10 + and: 'The values returned are there' + body.count == 15 + body.max == 10 + body.offset == 0 + body.sort == null + body.order == null + + and: 'the hal _links attribute is present' + body._links.size() == 4 + body._links.self.href.startsWith("${baseUrl}/product") + body._links.first.href.startsWith("${baseUrl}/product") + body._links.next.href.startsWith("${baseUrl}/product") + body._links.last.href.startsWith("${baseUrl}/product") + + and: 'the product is present' + body._embedded.products.size() == 10 cleanup: productsIds.each { id -> @@ -123,43 +147,48 @@ class ProductSpec extends HttpClientSpec { } } - void "test a middle page worth of products"() { + void 'test a middle page worth of products'() { given: def productsIds = [] 30.times { productNumber -> - ProductVM product = new ProductVM(name: "Product $productNumber", - description: "product ${productNumber} description", - price: productNumber + (productNumber / 100)) - HttpResponse createResp = client.toBlocking().exchange(HttpRequest.POST("/products", product), Map) + ProductVM product = new ProductVM( + name: "Product $productNumber", + description: "product ${productNumber} description", + price: productNumber + (productNumber / 100) + ) + HttpResponse createResp = client.toBlocking().exchange(HttpRequest.POST('/products', product), String) assert createResp.status == HttpStatus.CREATED - productsIds << createResp.body().id + Map createBody = objectMapper.readValue(createResp.body(), Map) + productsIds << createBody.id } - when: "We get the products" - HttpRequest request = HttpRequest.GET("/products?offset=10") - HttpResponse resp = client.toBlocking().exchange(request, Map) + when: 'We get the products' + HttpRequest request = HttpRequest.GET('/products?offset=10') + HttpResponse resp = client.toBlocking().exchange(request, String) + Map body = objectMapper.readValue(resp.body(), Map) then: resp.status == HttpStatus.OK resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/hal+json;charset=UTF-8' - and: "The values returned are there" - resp.body().count == 30 - resp.body().max == 10 - resp.body().offset == 10 - resp.body().sort == null - resp.body().order == null - and: "the hal _links attribute is present" - resp.body()._links.size() == 5 - resp.body()._links.self.href.startsWith("${baseUrl}/product") - resp.body()._links.first.href.startsWith("${baseUrl}/product") - resp.body()._links.prev.href.startsWith("${baseUrl}/product") - resp.body()._links.next.href.startsWith("${baseUrl}/product") - resp.body()._links.last.href.startsWith("${baseUrl}/product") - - and: "the product is present" - resp.body()._embedded.products.size() == 10 + and: 'The values returned are there' + body.count == 30 + body.max == 10 + body.offset == 10 + body.sort == null + body.order == null + + and: 'the hal _links attribute is present' + body._links.size() == 5 + body._links.self.href.startsWith("${baseUrl}/product") + body._links.first.href.startsWith("${baseUrl}/product") + body._links.prev.href.startsWith("${baseUrl}/product") + body._links.next.href.startsWith("${baseUrl}/product") + body._links.last.href.startsWith("${baseUrl}/product") + + and: 'the product is present' + body._embedded.products.size() == 10 cleanup: productsIds.each { id -> diff --git a/examples/functional-tests/src/integration-test/groovy/functional/tests/TeamSpec.groovy b/examples/functional-tests/src/integration-test/groovy/functional/tests/TeamSpec.groovy index da2ae2c85..6bd4a135e 100644 --- a/examples/functional-tests/src/integration-test/groovy/functional/tests/TeamSpec.groovy +++ b/examples/functional-tests/src/integration-test/groovy/functional/tests/TeamSpec.groovy @@ -1,5 +1,6 @@ package functional.tests +import com.fasterxml.jackson.databind.ObjectMapper import grails.testing.mixin.integration.Integration import grails.testing.spock.RunOnce import grails.web.http.HttpHeaders @@ -22,64 +23,163 @@ class TeamSpec extends HttpClientSpec { @Shared String lang + @Shared + ObjectMapper objectMapper + + void setup() { + objectMapper = new ObjectMapper() + } + void setupSpec() { this.lang = "${System.properties.getProperty('user.language')}_${System.properties.getProperty('user.country')}" } - void "Test association template rendering"() { + void 'Test association template rendering'() { when: - HttpRequest request = HttpRequest.GET("/teams/1") + HttpRequest request = HttpRequest.GET('/teams/1') HttpResponse resp = client.toBlocking().exchange(request, String) - then:"The response is correct" + then: 'The response is correct' resp.status == HttpStatus.OK resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' // Note current behaviour is that the captain is not rendered twice - resp.body() == '{"id":1,"name":"Barcelona","players":[{"id":1},{"id":2}],"captain":{"id":1},"sport":"football"}' + objectMapper.readTree(resp.body()) == objectMapper.readTree(''' + { + "id": 1, + "name": "Barcelona", + "players": [ + { "id": 1}, + { "id": 2} + ], + "captain": { "id":1 }, + "sport": "football" + } + ''') } - void "Test deep association template rendering"() { + void 'Test deep association template rendering'() { when: - HttpRequest request = HttpRequest.GET("/teams/deep/1") + HttpRequest request = HttpRequest.GET('/teams/deep/1') HttpResponse resp = client.toBlocking().exchange(request, String) - then:"The response is correct" + then: 'The response is correct' resp.status == HttpStatus.OK resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' - resp.body() == '{"id":1,"name":"Barcelona","players":[{"id":1,"name":"Iniesta","sport":"football"},{"id":2,"name":"Messi","sport":"football"}],"captain":{"id":1,"name":"Iniesta","sport":"football"},"sport":"football"}' + objectMapper.readTree(resp.body()) == objectMapper.readTree(''' + { + "id": 1, + "name": "Barcelona", + "players": [ + { "id": 1, "name": "Iniesta", "sport": "football" }, + { "id": 2, "name": "Messi", "sport": "football" } + ], + "captain": { "id": 1, "name": "Iniesta", "sport": "football" }, + "sport": "football" + } + ''') } - @IgnoreIf({ System.getenv("GITHUB_REF")}) - void "Test HAL rendering"() { + @IgnoreIf({ System.getenv('GITHUB_REF') }) + void 'Test HAL rendering'() { when: - HttpRequest request = HttpRequest.GET("/teams/hal/1") + HttpRequest request = HttpRequest.GET('/teams/hal/1') HttpResponse resp = client.toBlocking().exchange(request, String) - then:"The response is correct" + then: 'The response is correct' resp.status == HttpStatus.OK resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/hal+json;charset=UTF-8' - resp.body() == '{"_embedded":{"players":[{"_links":{"self":{"href":"http://localhost:'+serverPort+'/player/show/1","hreflang":"' + lang + '","type":"application/hal+json"}},"name":"Iniesta","version":0},{"_links":{"self":{"href":"http://localhost:'+serverPort+'/player/show/2","hreflang":"' + lang + '","type":"application/hal+json"}},"name":"Messi","version":0}],"captain":{"_links":{"self":{"href":"http://localhost:'+serverPort+'/player/show/1","hreflang":"' + lang + '","type":"application/hal+json"}},"name":"Iniesta","version":0}},"_links":{"self":{"href":"http://localhost:'+serverPort+'/teams/1","hreflang":"' + lang + '","type":"application/hal+json"}},"id":1,"name":"Barcelona","sport":"football","another":{"foo":"bar"}}' + objectMapper.readTree(resp.body()) == objectMapper.readTree(""" + { + \"_embedded\": { + \"players\": [ + { + \"_links\": { + \"self\": { + \"href\": \"http://localhost:$serverPort/player/show/1\", + \"hreflang\": \"$lang\", + \"type\": \"application/hal+json\" + } + }, + \"name\": \"Iniesta\", + \"version\": 0 + }, + { + \"_links\": { + \"self\": { + \"href\": \"http://localhost:$serverPort/player/show/2\", + \"hreflang\": \"$lang\", + \"type\": \"application/hal+json\" + } + }, + \"name\": \"Messi\", + \"version\": 0 + } + ], + \"captain\": { + \"_links\": { + \"self\": { + \"href\": \"http://localhost:$serverPort/player/show/1\", + \"hreflang\": \"$lang\", + \"type\": \"application/hal+json\" + } + }, + \"name\": \"Iniesta\", + \"version\": 0 + } + }, + \"_links\": { + \"self\": { + \"href\": \"http://localhost:$serverPort/teams/1\", + \"hreflang\": \"$lang\", + \"type\": \"application/hal+json\" + } + }, + \"id\": 1, + \"name\": \"Barcelona\", + \"sport\": \"football\", + \"another\": { + \"foo\": \"bar\" + } + } + """) } - void "Test composite ID rendering"() { + void 'Test composite ID rendering'() { + given: Composite.withNewSession { Composite.withNewTransaction { - new Composite(name: "foo", team: Team.load(1), player: Player.load(2)).save(flush: true, failOnError: true) + new Composite(name: 'foo', team: Team.load(1), player: Player.load(2)).save(flush: true, failOnError: true) } } + when: - HttpRequest request = HttpRequest.GET("/team/composite") + HttpRequest request = HttpRequest.GET('/team/composite') HttpResponse resp = client.toBlocking().exchange(request, String) - then:"The response is correct" + then: 'The response is correct' resp.status == HttpStatus.OK resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() resp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' - resp.body() == '{"player":{"id":2,"name":"Messi","sport":"football"},"team":{"id":1,"name":"Barcelona","captain":{"id":1},"sport":"football"},"name":"foo"}' + objectMapper.readTree(resp.body()) == objectMapper.readTree(''' + { + "player": { + "id": 2, + "name": "Messi", + "sport": "football" + }, + "team": { + "id": 1, + "name": "Barcelona", + "captain": { "id": 1 }, + "sport": "football" + }, + "name":"foo" + } + ''') } } diff --git a/examples/functional-tests/src/integration-test/groovy/functional/tests/TestGsonControllerSpec.groovy b/examples/functional-tests/src/integration-test/groovy/functional/tests/TestGsonControllerSpec.groovy index 33b879aa1..db252eb33 100644 --- a/examples/functional-tests/src/integration-test/groovy/functional/tests/TestGsonControllerSpec.groovy +++ b/examples/functional-tests/src/integration-test/groovy/functional/tests/TestGsonControllerSpec.groovy @@ -1,112 +1,167 @@ package functional.tests +import com.fasterxml.jackson.databind.ObjectMapper import grails.testing.mixin.integration.Integration import grails.testing.spock.RunOnce import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse import org.junit.jupiter.api.BeforeEach +import spock.lang.Shared @Integration(applicationClass = Application) class TestGsonControllerSpec extends HttpClientSpec { + @Shared + ObjectMapper objectMapper + + def setup() { + objectMapper = new ObjectMapper() + } + @RunOnce @BeforeEach void init() { super.init() } - void "Test that responding with a map is possible"() { - when:"When JSON is requested" - HttpRequest request = HttpRequest.GET("/testGson/testRespondWithMap") + void 'Test that responding with a map is possible'() { + when: 'When JSON is requested' + HttpRequest request = HttpRequest.GET('/testGson/testRespondWithMap') HttpResponse resp = client.toBlocking().exchange(request, String) - then:"The JSON view is rendered" + then: 'The JSON view is rendered' resp.body() == '{"message":"two"}' } - void "Test that responding with a map is possible with object template"() { - when:"When JSON is requested" - HttpRequest request = HttpRequest.GET("/testGson/testRespondWithMapObjectTemplate.json") + void 'Test that responding with a map is possible with object template'() { + when: 'When JSON is requested' + HttpRequest request = HttpRequest.GET('/testGson/testRespondWithMapObjectTemplate.json') HttpResponse resp = client.toBlocking().exchange(request, String) - then:"The JSON view is rendered" + then: 'The JSON view is rendered' resp.body() == '{"one":"two"}' } - void "Test that it is possible to use the template engine directly"() { - when:"When JSON is requested" - HttpRequest request = HttpRequest.GET("/testGson/testTemplateEngine") + + void 'Test that it is possible to use the template engine directly'() { + when: 'When JSON is requested' + HttpRequest request = HttpRequest.GET('/testGson/testTemplateEngine') HttpResponse resp = client.toBlocking().exchange(request, String) - then:"The JSON view is rendered" - resp.body() == '{"title":"The Stand","timeZone":"America/New_York","vendor":"MyCompany"}' + then: 'The JSON view is rendered' + objectMapper.readTree(resp.body()) == objectMapper.readTree(''' + { + "title": "The Stand", + "timeZone": "America/New_York", + "vendor": "MyCompany" + } + ''') } - void "Test the respond method returns a GSON view for JSON request"() { - when:"When JSON is requested" - HttpRequest request = HttpRequest.GET("/testGson/testRespond.json") + void 'Test the respond method returns a GSON view for JSON request'() { + when: 'When JSON is requested' + HttpRequest request = HttpRequest.GET('/testGson/testRespond.json') HttpResponse resp = client.toBlocking().exchange(request, String) - then:"The JSON view is rendered" + then:'The JSON view is rendered' resp.body() == '{"test":{"name":"Bob"}}' - when:"When HTML is requested" - request = HttpRequest.GET("/testGson/testRespond.html") + when: 'When HTML is requested' + request = HttpRequest.GET('/testGson/testRespond.html') resp = client.toBlocking().exchange(request, String) - then:"The GSP is rendered" + then:'The GSP is rendered' resp.body().contains('

Test Bob HTML

') } - void "Test the respond method returns a GSON named after the domain view for JSON request"() { - when:"When JSON is requested" - HttpRequest request = HttpRequest.GET("/testGson/testRespondWithTemplateForDomain.json") + void 'Test the respond method returns a GSON named after the domain view for JSON request'() { + when: 'When JSON is requested' + HttpRequest request = HttpRequest.GET('/testGson/testRespondWithTemplateForDomain.json') HttpResponse resp = client.toBlocking().exchange(request, String) - then:"The JSON view is rendered" - resp.body() == '{"test":{"name":"Bob","age":60}}' + then: 'The JSON view is rendered' + objectMapper.readTree(resp.body()) == objectMapper.readTree(''' + { + "test": { + "name": "Bob", + "age": 60 + } + } + ''') } - void "Test template rendering works"() { - when:"A view that renders templates is rendered" - HttpRequest request = HttpRequest.GET("/testGson/testTemplate.json") + void 'Test template rendering works'() { + when: 'A view that renders templates is rendered' + HttpRequest request = HttpRequest.GET('/testGson/testTemplate.json') HttpResponse resp = client.toBlocking().exchange(request, String) - then:"The result is correct" - resp.body() == '{"test":{"name":"Bob","child":{"child":{"name":"Joe","age":10}},"children":[{"child":{"name":"Joe","age":10}}]}}' + then: 'The result is correct' + objectMapper.readTree(resp.body()) == objectMapper.readTree(''' + { + "test": { + "name": "Bob", + "child": { + "child": { + "name": "Joe", + "age": 10 + } + }, + "children": [ + { + "child": { + "name": "Joe", + "age": 10 + } + } + ] + } + } + ''') } - void "Test views from plugins are rendered"() { - when:"A view that renders templates is rendered" - HttpRequest request = HttpRequest.GET("/testGson/testGsonFromPlugin") + void 'Test views from plugins are rendered'() { + when: 'A view that renders templates is rendered' + HttpRequest request = HttpRequest.GET('/testGson/testGsonFromPlugin') HttpResponse resp = client.toBlocking().exchange(request, String) - then:"The result is correct" + then: 'The result is correct' resp.body() == '{"message":"Hello from Plugin"}' } - void "Test view that inherits from plugins are rendered"() { + void 'Test view that inherits from plugins are rendered'() { when: - HttpRequest request = HttpRequest.GET("/testGson/testInheritsFromPlugin") + HttpRequest request = HttpRequest.GET('/testGson/testInheritsFromPlugin') HttpResponse resp = client.toBlocking().exchange(request, String) then: - resp.body() == '{"message":"Hello from Plugin Template","foo":"bar"}' + objectMapper.readTree(resp.body()) == objectMapper.readTree(''' + { + "message": "Hello from Plugin Template", + "foo": "bar" + } + ''') } - void "Test augmenting model"() { - when:"When JSON is requested" - HttpRequest request = HttpRequest.GET("/testGson/testAugmentModel.json") + void 'Test augmenting model'() { + when: 'When JSON is requested' + HttpRequest request = HttpRequest.GET('/testGson/testAugmentModel.json') HttpResponse resp = client.toBlocking().exchange(request, String) - then:"The JSON view is rendered" - resp.body() == '{"test":{"name":"John","age":20}}' - - when:"When HTML is requested" - request = HttpRequest.GET("/testGson/testAugmentModel.html") + then: 'The JSON view is rendered' + objectMapper.readTree(resp.body()) == objectMapper.readTree(''' + { + "test": { + "name": "John", + "age": 20 + } + } + ''') + + when: 'When HTML is requested' + request = HttpRequest.GET('/testGson/testAugmentModel.html') resp = client.toBlocking().exchange(request, String) - then:"The GSP is rendered" + then: 'The GSP is rendered' resp.body().contains("

Test John (20) HTML

") } } diff --git a/examples/functional-tests/src/integration-test/groovy/functional/tests/VehicleSpec.groovy b/examples/functional-tests/src/integration-test/groovy/functional/tests/VehicleSpec.groovy index 3cf553330..dde4e3e20 100644 --- a/examples/functional-tests/src/integration-test/groovy/functional/tests/VehicleSpec.groovy +++ b/examples/functional-tests/src/integration-test/groovy/functional/tests/VehicleSpec.groovy @@ -6,6 +6,7 @@ import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus import org.junit.jupiter.api.BeforeEach +import spock.lang.PendingFeature @Integration(applicationClass = Application) class VehicleSpec extends HttpClientSpec { @@ -16,6 +17,7 @@ class VehicleSpec extends HttpClientSpec { super.init() } + @PendingFeature(reason = 'GORM inheritance not working in Groovy 4') void "Test that domain subclasses render their properties"() { when: HttpRequest request = HttpRequest.GET('/vehicle/list') @@ -27,6 +29,7 @@ class VehicleSpec extends HttpClientSpec { } + @PendingFeature(reason = 'GORM inheritance not working in Groovy 4') void "Test that domain association subclasses render their properties"() { when: HttpRequest request = HttpRequest.GET('/vehicle/garage') diff --git a/examples/functional-tests/src/integration-test/groovy/functional/tests/api/NamespacedBookSpec.groovy b/examples/functional-tests/src/integration-test/groovy/functional/tests/api/NamespacedBookSpec.groovy index fdf014b0a..c19a57851 100644 --- a/examples/functional-tests/src/integration-test/groovy/functional/tests/api/NamespacedBookSpec.groovy +++ b/examples/functional-tests/src/integration-test/groovy/functional/tests/api/NamespacedBookSpec.groovy @@ -1,5 +1,6 @@ package functional.tests.api +import com.fasterxml.jackson.databind.ObjectMapper import functional.tests.Application import functional.tests.HttpClientSpec import grails.testing.mixin.integration.Integration @@ -11,119 +12,127 @@ import io.micronaut.http.HttpStatus import io.micronaut.http.MediaType import org.junit.jupiter.api.BeforeEach import spock.lang.Issue +import spock.lang.Shared @Integration(applicationClass = Application) class NamespacedBookSpec extends HttpClientSpec { + @Shared + ObjectMapper objectMapper + + def setup() { + objectMapper = new ObjectMapper() + } + @RunOnce @BeforeEach void init() { super.init() } - void "test view rendering with a namespace"() { - when: "A request is sent to a controller with a namespace" + void 'test view rendering with a namespace'() { + when: 'A request is sent to a controller with a namespace' HttpRequest request = HttpRequest.GET('/api/book') HttpResponse rsp = client.toBlocking().exchange(request, Map) - then: "The rsponse is correct" + then: 'The rsponse is correct' rsp.status() == HttpStatus.OK rsp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() rsp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' - rsp.body().api == "version 1.0 (Namespaced)" - rsp.body().title == "API - The Shining" + rsp.body().api == 'version 1.0 (Namespaced)' + rsp.body().title == 'API - The Shining' } - void "test nested template rendering with a namespace"() { - when: "A request is sent to a controller with a namespace" + void 'test nested template rendering with a namespace'() { + when: 'A request is sent to a controller with a namespace' HttpRequest request = HttpRequest.GET('/api/book/nested') HttpResponse rsp = client.toBlocking().exchange(request, Map) - - then: "The rsponse contains the child template" + then: 'The rsponse contains the child template' rsp.status() == HttpStatus.OK rsp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() rsp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' - rsp.body().foo == "bar" + rsp.body().foo == 'bar' } - void "test the correct content type is chosen (json)"() { - when: "A request is sent to a controller with a namespace" + void 'test the correct content type is chosen (json)'() { + when: 'A request is sent to a controller with a namespace' HttpRequest request = HttpRequest.GET('/api/book') HttpResponse rsp = client.toBlocking().exchange(request, Map) - then: "The rsponse contains the child template" + then: 'The response contains the child template' rsp.status() == HttpStatus.OK rsp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() rsp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' !rsp.body()['_links'] - rsp.body().api == "version 1.0 (Namespaced)" - rsp.body().title == "API - The Shining" + rsp.body().api == 'version 1.0 (Namespaced)' + rsp.body().title == 'API - The Shining' } - void "test the correct content type is chosen (hal)"() { - when: "A request is sent to a controller with a namespace" + void 'test the correct content type is chosen (hal)'() { + when: 'A request is sent to a controller with a namespace' HttpRequest request = HttpRequest.GET('/api/book').accept(MediaType.APPLICATION_HAL_JSON_TYPE) - HttpResponse rsp = client.toBlocking().exchange(request, Map) + HttpResponse rsp = client.toBlocking().exchange(request, String) + Map body = objectMapper.readValue(rsp.body(), Map) - then: "The rsponse contains the child template" + then: 'The response contains the child template' rsp.status() == HttpStatus.OK rsp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() rsp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/hal+json;charset=UTF-8' - rsp.body()['_links'] - rsp.body().api == "version 1.0 (Namespaced HAL)" - rsp.body().title == "API - The Shining" + body['_links'] + body.api == 'version 1.0 (Namespaced HAL)' + body.title == 'API - The Shining' } - void "test render(view: '..', model: ..) in controllers with namespaces works"() { - when: "A request is sent to a controller with a namespace" + void 'test render(view: "..", model: ..) in controllers with namespaces works'() { + when: 'A request is sent to a controller with a namespace' HttpRequest request = HttpRequest.GET('/api/book/testRender') HttpResponse rsp = client.toBlocking().exchange(request, Map) - then: "The rsponse is correct" + then: 'The rsponse is correct' rsp.status() == HttpStatus.OK rsp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() rsp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' - rsp.body().api == "version 1.0 (Namespaced)" - rsp.body().title == "API - The Shining" + rsp.body().api == 'version 1.0 (Namespaced)' + rsp.body().title == 'API - The Shining' } - void "test rspond(foo, view: ..) in controllers with namespaces works"() { - when: "A request is sent to a controller with a namespace" + void 'test rspond(foo, view: ..) in controllers with namespaces works'() { + when: 'A request is sent to a controller with a namespace' HttpRequest request = HttpRequest.GET('/api/book/testRespond') HttpResponse rsp = client.toBlocking().exchange(request, Map) - then: "The rsponse is correct" + then: 'The response is correct' rsp.status() == HttpStatus.OK rsp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() rsp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' - rsp.body().api == "version 1.0 (Namespaced)" - rsp.body().title == "API - The Shining" + rsp.body().api == 'version 1.0 (Namespaced)' + rsp.body().title == 'API - The Shining' } - void "test rspond(foo, view: ..) in controllers with namespaces works, view outside of namespace"() { - when: "A request is sent to a controller with a namespace" + void 'test respond(foo, view: ..) in controllers with namespaces works, view outside of namespace'() { + when: 'A request is sent to a controller with a namespace' HttpRequest request = HttpRequest.GET('/api/book/testRespondOutsideNamespace') HttpResponse rsp = client.toBlocking().exchange(request, Map) - then: "The rsponse is correct" + then: 'The response is correct' rsp.status() == HttpStatus.OK rsp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() rsp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' - rsp.body().api == "version 1.0 (Non-Namespaced)" - rsp.body().title == "API - The Shining" + rsp.body().api == 'version 1.0 (Non-Namespaced)' + rsp.body().title == 'API - The Shining' } - @Issue("https://github.com/grails/grails-views/issues/186") - void "test view rendering with a namespace from a map"() { - when: "A request is sent to a controller with a namespace" + @Issue('https://github.com/grails/grails-views/issues/186') + void 'test view rendering with a namespace from a map'() { + when: 'A request is sent to a controller with a namespace' HttpRequest request = HttpRequest.GET('/api/book/message') HttpResponse rsp = client.toBlocking().exchange(request, Map) - then: "The rsponse is correct" + then: 'The response is correct' rsp.status() == HttpStatus.OK rsp.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() rsp.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' - rsp.body().message == "Controller says Hello API" + rsp.body().message == 'Controller says Hello API' } } diff --git a/examples/functional-tests/src/integration-test/resources/logback-test.xml b/examples/functional-tests/src/integration-test/resources/logback-test.xml new file mode 100644 index 000000000..8b10f2fe9 --- /dev/null +++ b/examples/functional-tests/src/integration-test/resources/logback-test.xml @@ -0,0 +1,25 @@ + + + + + + + + + UTF-8 + %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wex + + + + + + + + + + + \ No newline at end of file diff --git a/gradle-plugin/src/main/groovy/grails/views/gradle/AbstractGroovyTemplateCompileTask.groovy b/gradle-plugin/src/main/groovy/grails/views/gradle/AbstractGroovyTemplateCompileTask.groovy index 32e35f6c8..a24ea1cce 100644 --- a/gradle-plugin/src/main/groovy/grails/views/gradle/AbstractGroovyTemplateCompileTask.groovy +++ b/gradle-plugin/src/main/groovy/grails/views/gradle/AbstractGroovyTemplateCompileTask.groovy @@ -10,7 +10,6 @@ import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.Nested import org.gradle.api.tasks.Optional -import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.compile.AbstractCompile import org.gradle.process.ExecResult @@ -38,11 +37,23 @@ abstract class AbstractGroovyTemplateCompileTask extends AbstractCompile { @Nested final ViewCompileOptions compileOptions + @Input + final Property fileExtension + + @Input + final Property scriptBaseName + + @Input + final Property compilerName + @Inject AbstractGroovyTemplateCompileTask(ObjectFactory objectFactory) { - packageName = objectFactory.property(String) + packageName = objectFactory.property(String).convention(project.name ?: project.projectDir.canonicalFile.name) srcDir = objectFactory.directoryProperty() compileOptions = new ViewCompileOptions(objectFactory) + fileExtension = objectFactory.property(String) + scriptBaseName = objectFactory.property(String) + compilerName = objectFactory.property(String) } @Override @@ -62,16 +73,12 @@ abstract class AbstractGroovyTemplateCompileTask extends AbstractCompile { protected void compile() { Iterable projectPackageNames = getProjectPackageNames(project.projectDir) - if (packageName.isPresent()) { - packageName.set(project.name ?: project.projectDir.canonicalFile.name) - } - ExecResult result = project.javaexec( new Action() { @Override @CompileDynamic void execute(JavaExecSpec javaExecSpec) { - javaExecSpec.mainClass.set(getCompilerName()) - javaExecSpec.classpath = getClasspath() + javaExecSpec.mainClass.set(compilerName) + javaExecSpec.classpath = classpath List jvmArgs = compileOptions.forkOptions.jvmArgs if (jvmArgs) { @@ -88,7 +95,7 @@ abstract class AbstractGroovyTemplateCompileTask extends AbstractCompile { packageImports, packageName.get(), project.file('grails-app/conf/application.yml').canonicalPath, - compileOptions.encoding + compileOptions.encoding.get() ] as List prepareArguments(arguments) @@ -103,17 +110,6 @@ abstract class AbstractGroovyTemplateCompileTask extends AbstractCompile { // no-op } - @Input - protected String getCompilerName() { - 'grails.views.GenericGroovyTemplateCompiler' - } - - @Input - abstract String getFileExtension() - - @Input - abstract String getScriptBaseName() - Iterable getProjectPackageNames(File baseDir) { File rootDir = baseDir ? new File(baseDir, "grails-app${File.separator}domain") : null Set packageNames = [] diff --git a/gradle-plugin/src/main/groovy/grails/views/gradle/AbstractGroovyTemplatePlugin.groovy b/gradle-plugin/src/main/groovy/grails/views/gradle/AbstractGroovyTemplatePlugin.groovy index 880b33477..2d2acf4f4 100644 --- a/gradle-plugin/src/main/groovy/grails/views/gradle/AbstractGroovyTemplatePlugin.groovy +++ b/gradle-plugin/src/main/groovy/grails/views/gradle/AbstractGroovyTemplatePlugin.groovy @@ -7,8 +7,10 @@ import org.apache.tools.ant.taskdefs.condition.Os import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.Task +import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.FileCollection import org.gradle.api.tasks.SourceSetOutput +import org.gradle.api.tasks.TaskContainer import org.gradle.api.tasks.bundling.Jar import org.grails.gradle.plugin.core.GrailsExtension import org.grails.gradle.plugin.core.IntegrationTestGradlePlugin @@ -32,7 +34,7 @@ class AbstractGroovyTemplatePlugin implements Plugin { AbstractGroovyTemplatePlugin(Class taskClass, String fileExtension) { this.taskClass = taskClass this.fileExtension = fileExtension - this.pathToSource = "grails-app/views" + this.pathToSource = 'grails-app/views' } AbstractGroovyTemplatePlugin(Class taskClass, String fileExtension, String pathToSource) { @@ -43,43 +45,58 @@ class AbstractGroovyTemplatePlugin implements Plugin { @Override void apply(Project project) { - def allTasks = project.tasks - def upperCaseName = GrailsNameUtils.getClassName(fileExtension) - AbstractGroovyTemplateCompileTask templateCompileTask = (AbstractGroovyTemplateCompileTask) allTasks.register("compile${upperCaseName}Views".toString(), (Class) taskClass).get() + TaskContainer tasks = project.tasks + String upperCaseName = GrailsNameUtils.getClassName(fileExtension) + AbstractGroovyTemplateCompileTask templateCompileTask = (AbstractGroovyTemplateCompileTask) tasks.register( + "compile${upperCaseName}Views".toString(), + (Class) taskClass + ).get() SourceSetOutput output = SourceSets.findMainSourceSet(project)?.output FileCollection classesDir = resolveClassesDirs(output, project) - File destDir = new File(project.buildDir, "${templateCompileTask.fileExtension}-classes/main") + File destDir = new File(project.layout.buildDirectory.get().asFile, "${templateCompileTask.fileExtension.get()}-classes/main") output?.dir(destDir) project.afterEvaluate { GrailsExtension grailsExt = project.extensions.getByType(GrailsExtension) if (grailsExt.pathingJar && Os.isFamily(Os.FAMILY_WINDOWS)) { - Jar pathingJar = (Jar) allTasks.named('pathingJar').get() - def allClasspath = project.files("${project.buildDir}/classes/groovy/main", "${project.buildDir}/resources/main", "${project.projectDir}/gsp-classes", pathingJar.archiveFile.get().asFile) + Jar pathingJar = (Jar) tasks.named('pathingJar').get() + ConfigurableFileCollection allClasspath = project.files( + "${project.layout.buildDirectory.get().asFile}/classes/groovy/main", + "${project.layout.buildDirectory.get().asFile}/resources/main", + "${project.layout.projectDirectory.getAsFile()}/gsp-classes", + pathingJar.archiveFile.get().asFile + ) templateCompileTask.dependsOn(pathingJar) templateCompileTask.classpath = allClasspath } } def allClasspath = classesDir + project.configurations.named('compileClasspath').get() - templateCompileTask.getDestinationDirectory().set( destDir ) + templateCompileTask.destinationDirectory.set(destDir) templateCompileTask.classpath = allClasspath templateCompileTask.packageName.set(project.name) templateCompileTask.setSource(project.file("${project.projectDir}/$pathToSource")) - templateCompileTask.dependsOn( allTasks.named('classes').get() ) + templateCompileTask.dependsOn(tasks.named('classes').get()) project.plugins.withType(SpringBootPlugin).configureEach {plugin -> - allTasks.withType(Jar).configureEach { Task task -> - if (task.name in ['jar', 'bootJar', 'war', 'bootWar']) { task.dependsOn templateCompileTask } + tasks.withType(Jar).configureEach { Task task -> + if (task.name in ['jar', 'bootJar', 'war', 'bootWar']) { + task.dependsOn(templateCompileTask) + } + } + tasks.withType(ResolveMainClassName).configureEach { + it.dependsOn(templateCompileTask) } - allTasks.withType(ResolveMainClassName).configureEach { t -> t.dependsOn(templateCompileTask)} } - project.plugins.withType(IntegrationTestGradlePlugin).configureEach { plugin -> - allTasks.named("compileIntegrationTestGroovy") { t-> t.dependsOn(templateCompileTask)} - allTasks.named("integrationTest") {t -> t.dependsOn(templateCompileTask)} + project.plugins.withType(IntegrationTestGradlePlugin).configureEach { + tasks.named('compileIntegrationTestGroovy') { Task task -> + task.dependsOn(templateCompileTask) + } + tasks.named('integrationTest') { Task task -> + task.dependsOn(templateCompileTask) + } } } @CompileDynamic protected FileCollection resolveClassesDirs(SourceSetOutput output, Project project) { - return output.classesDirs ?: project.files(new File(project.buildDir, "classes/groovy/main")) + return output.classesDirs ?: project.files(new File(project.layout.buildDirectory.get().asFile, 'classes/groovy/main')) } - } diff --git a/gradle-plugin/src/main/groovy/grails/views/gradle/ViewCompileOptions.groovy b/gradle-plugin/src/main/groovy/grails/views/gradle/ViewCompileOptions.groovy index 8175bd1af..056840f7b 100644 --- a/gradle-plugin/src/main/groovy/grails/views/gradle/ViewCompileOptions.groovy +++ b/gradle-plugin/src/main/groovy/grails/views/gradle/ViewCompileOptions.groovy @@ -5,7 +5,7 @@ import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.Nested import org.gradle.api.tasks.compile.GroovyForkOptions -import javax.inject.Inject; +import javax.inject.Inject /** * @author Graeme Rocher diff --git a/gradle-plugin/src/main/groovy/grails/views/gradle/json/GrailsJsonViewsPlugin.groovy b/gradle-plugin/src/main/groovy/grails/views/gradle/json/GrailsJsonViewsPlugin.groovy index d577f0869..7d0248b58 100644 --- a/gradle-plugin/src/main/groovy/grails/views/gradle/json/GrailsJsonViewsPlugin.groovy +++ b/gradle-plugin/src/main/groovy/grails/views/gradle/json/GrailsJsonViewsPlugin.groovy @@ -13,7 +13,7 @@ import groovy.transform.CompileStatic class GrailsJsonViewsPlugin extends AbstractGroovyTemplatePlugin { GrailsJsonViewsPlugin() { - super(JsonViewCompilerTask, "gson") + super(JsonViewCompilerTask, 'gson') } } diff --git a/gradle-plugin/src/main/groovy/grails/views/gradle/json/JsonViewCompilerTask.groovy b/gradle-plugin/src/main/groovy/grails/views/gradle/json/JsonViewCompilerTask.groovy index 318159bc1..485d9ef2a 100644 --- a/gradle-plugin/src/main/groovy/grails/views/gradle/json/JsonViewCompilerTask.groovy +++ b/gradle-plugin/src/main/groovy/grails/views/gradle/json/JsonViewCompilerTask.groovy @@ -2,8 +2,12 @@ package grails.views.gradle.json import grails.views.gradle.AbstractGroovyTemplateCompileTask import groovy.transform.CompileStatic +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property import org.gradle.api.tasks.Input +import javax.inject.Inject + /** * Concrete implementation that compiles JSON templates * @@ -12,22 +16,11 @@ import org.gradle.api.tasks.Input @CompileStatic class JsonViewCompilerTask extends AbstractGroovyTemplateCompileTask { - @Input - @Override - String getFileExtension() { - "gson" - } - - @Input - @Override - String getScriptBaseName() { - "grails.plugin.json.view.JsonViewTemplate" + @Inject + JsonViewCompilerTask(ObjectFactory objectFactory) { + super(objectFactory) + fileExtension.convention('gson') + scriptBaseName.convention('grails.plugin.json.view.JsonViewTemplate') + compilerName.convention('grails.plugin.json.view.JsonViewCompiler') } - - @Input - @Override - protected String getCompilerName() { - "grails.plugin.json.view.JsonViewCompiler" - } - } diff --git a/gradle-plugin/src/main/groovy/grails/views/gradle/markup/GrailsMarkupViewsPlugin.groovy b/gradle-plugin/src/main/groovy/grails/views/gradle/markup/GrailsMarkupViewsPlugin.groovy index baf6eede3..2d0a1f9a7 100644 --- a/gradle-plugin/src/main/groovy/grails/views/gradle/markup/GrailsMarkupViewsPlugin.groovy +++ b/gradle-plugin/src/main/groovy/grails/views/gradle/markup/GrailsMarkupViewsPlugin.groovy @@ -13,6 +13,6 @@ import groovy.transform.CompileStatic class GrailsMarkupViewsPlugin extends AbstractGroovyTemplatePlugin { GrailsMarkupViewsPlugin() { - super(MarkupViewCompilerTask, "gml") + super(MarkupViewCompilerTask, 'gml') } } diff --git a/gradle-plugin/src/main/groovy/grails/views/gradle/markup/MarkupViewCompilerTask.groovy b/gradle-plugin/src/main/groovy/grails/views/gradle/markup/MarkupViewCompilerTask.groovy index 27bf8b598..fe379727f 100644 --- a/gradle-plugin/src/main/groovy/grails/views/gradle/markup/MarkupViewCompilerTask.groovy +++ b/gradle-plugin/src/main/groovy/grails/views/gradle/markup/MarkupViewCompilerTask.groovy @@ -2,6 +2,8 @@ package grails.views.gradle.markup import grails.views.gradle.AbstractGroovyTemplateCompileTask import groovy.transform.CompileStatic +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property import org.gradle.api.tasks.Input /** @@ -14,21 +16,18 @@ import org.gradle.api.tasks.Input class MarkupViewCompilerTask extends AbstractGroovyTemplateCompileTask { @Input - @Override - String getFileExtension() { - "gml" - } + final Property fileExtension @Input - @Override - String getScriptBaseName() { - "grails.plugin.markup.view.MarkupViewTemplate" - } + final Property scriptBaseName @Input - @Override - protected String getCompilerName() { - "grails.plugin.markup.view.MarkupViewCompiler" - } + final Property compilerName + MarkupViewCompilerTask(ObjectFactory objectFactory) { + super(objectFactory) + fileExtension.convention('gml') + scriptBaseName.convention('grails.plugin.markup.view.MarkupViewTemplate') + compilerName.convention('grails.plugin.markup.view.MarkupViewCompiler') + } } diff --git a/gradle/buildsrc.libs.versions.toml b/gradle/buildsrc.libs.versions.toml index 58ea0ea59..bc230e16b 100644 --- a/gradle/buildsrc.libs.versions.toml +++ b/gradle/buildsrc.libs.versions.toml @@ -1,6 +1,6 @@ [versions] asciidoctor-gradle-jvm = '4.0.2' -assetpipeline = '4.5.1' +assetpipeline = '5.0.1' grails-gradle-plugin = '7.0.0-SNAPSHOT' grails-views = '4.0.0-SNAPSHOT' groovy-doc = '1.0.1' diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8f899e282..35f961ef1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -assetpipeline = '4.5.1' +assetpipeline = '5.0.1' caffeine = '2.9.3' gorm = '9.0.0-SNAPSHOT' gorm-hibernate5 = '9.0.0-SNAPSHOT' @@ -9,16 +9,16 @@ grails-gradle-plugin = '7.0.0-SNAPSHOT' grails-testing-support = '4.0.0-SNAPSHOT' groovy = '4.0.23' java-baseline = '17' -jackson-databind = '2.17.2' +jackson-databind = '2.18.0' jakarta-annotation-api = '3.0.0' jakarta-servlet-api = '6.0.0' jakarta-validation-api = '3.0.2' -micronaut = '4.5.3' +micronaut = '4.6.5' mongodb = '4.11.2' -slf4j = '1.7.36' +slf4j = '2.0.16' spock = '2.3-groovy-4.0' -spring = '6.1.8' -spring-boot = '3.2.6' +spring = '6.1.13' +spring-boot = '3.3.4' [libraries] assetpipeline = { module = 'com.bertramlabs.plugins:asset-pipeline-grails', version.ref = 'assetpipeline' } @@ -43,6 +43,7 @@ jackson-databind = { module = 'com.fasterxml.jackson.core:jackson-databind', ver jakarta-annotation-api = { module = 'jakarta.annotation:jakarta.annotation-api', version.ref = 'jakarta-annotation-api' } jakarta-servlet-api = { module = 'jakarta.servlet:jakarta.servlet-api', version.ref = 'jakarta-servlet-api' } jakarta-validation-api = { module = 'jakarta.validation:jakarta.validation-api', version.ref = 'jakarta-validation-api' } +micronaut-jackson-databind = { module = 'io.micronaut:micronaut-jackson-databind', version.ref = 'micronaut' } micronaut-http-client = { module = 'io.micronaut:micronaut-http-client', version.ref = 'micronaut' } mongodb-bson = { module = 'org.mongodb:bson', version.ref = 'mongodb' } slf4j-api = { module = 'org.slf4j:slf4j-api', version.ref = 'slf4j' } diff --git a/json/build.gradle b/json/build.gradle index ac83ab3c3..d8f35f8ee 100644 --- a/json/build.gradle +++ b/json/build.gradle @@ -20,6 +20,7 @@ dependencies { implementation libs.groovy.json implementation libs.jakarta.validation.api + testImplementation project(':views-json-testing-support') testImplementation libs.grails.testing.support.core testImplementation libs.grails.testing.support.gorm testImplementation libs.grails.datastore.gorm.hibernate5 diff --git a/json/src/main/groovy/grails/plugin/json/view/JsonViewCompiler.groovy b/json/src/main/groovy/grails/plugin/json/view/JsonViewCompiler.groovy index 53ed32f1e..e4b215a17 100644 --- a/json/src/main/groovy/grails/plugin/json/view/JsonViewCompiler.groovy +++ b/json/src/main/groovy/grails/plugin/json/view/JsonViewCompiler.groovy @@ -7,6 +7,7 @@ import groovy.transform.CompileStatic import groovy.transform.InheritConstructors import org.codehaus.groovy.control.CompilerConfiguration import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer + /** * A compiler for JSON views * @@ -22,7 +23,14 @@ class JsonViewCompiler extends AbstractGroovyTemplateCompiler { CompilerConfiguration compiler = super.configureCompiler(configuration) if(viewConfiguration.compileStatic) { configuration.addCompilationCustomizers( - new ASTTransformationCustomizer(Collections.singletonMap("extensions", "grails.plugin.json.view.internal.JsonTemplateTypeCheckingExtension"), CompileStatic.class)) + new ASTTransformationCustomizer( + Collections.singletonMap( + 'extensions', + 'grails.plugin.json.view.internal.JsonTemplateTypeCheckingExtension' + ), + CompileStatic + ) + ) } configuration.setScriptBaseClass( viewConfiguration.baseTemplateClass.name @@ -32,12 +40,10 @@ class JsonViewCompiler extends AbstractGroovyTemplateCompiler { @Override protected ViewsTransform newViewsTransform() { - return new JsonViewsTransform(this.viewConfiguration.extension) + return new JsonViewsTransform(viewConfiguration.extension) } - static void main(String[] args) { run(args, JsonViewConfiguration, JsonViewCompiler) } - } diff --git a/json/src/test/groovy/grails/plugin/json/view/EnumRenderingSpec.groovy b/json/src/test/groovy/grails/plugin/json/view/EnumRenderingSpec.groovy index 7b59a3a7b..793278245 100644 --- a/json/src/test/groovy/grails/plugin/json/view/EnumRenderingSpec.groovy +++ b/json/src/test/groovy/grails/plugin/json/view/EnumRenderingSpec.groovy @@ -1,53 +1,77 @@ package grails.plugin.json.view +import com.fasterxml.jackson.databind.ObjectMapper import grails.persistence.Entity -import grails.plugin.json.view.test.JsonViewTest -import org.grails.testing.GrailsUnitTest +import grails.views.json.test.JsonViewUnitTest +import spock.lang.Shared import spock.lang.Specification -class EnumRenderingSpec extends Specification implements JsonViewTest, GrailsUnitTest { +class EnumRenderingSpec extends Specification implements JsonViewUnitTest { - void "Test the render method when a domain instance defines an enum"() { - when:"rendering an object that defines an enum" + @Shared + ObjectMapper objectMapper + + void setupSpec() { + objectMapper = new ObjectMapper() + } + + void 'Test the render method when a domain instance defines an enum'() { + when: 'rendering an object that defines an enum' mappingContext.addPersistentEntity(EnumTest) def result = render(''' -model { - Object object -} -json g.render(object) -''', [object: new EnumTest(name:"Fred", bar: TestEnum.BAR)]) - then:"the json is rendered correctly" - result.json.bar == "BAR" - result.json.name == "Fred" + model { + Object object + } + json g.render(object) + ''', [object: new EnumTest(name: 'Fred', bar: TestEnum.BAR)]) + + then: 'the json is rendered correctly' + result.json['bar'] == 'BAR' + result.json['name'] == 'Fred' } - void "Test the render method when a PGOO instance defines an enum"() { - when:"rendering an object that defines an enum" + void 'Test the render method when a POGO instance defines an enum'() { + when: 'rendering an object that defines an enum' def result = render(''' -model { - Object object -} -json g.render(object) -''', [object: new EnumTest(name:"Fred", bar: TestEnum.BAR)]) - then:"the json is rendered correctly" - result.json.bar == "BAR" - result.json.name == "Fred" + model { + Object object + } + json g.render(object) + ''', [object: new EnumTest(name:'Fred', bar: TestEnum.BAR)]) + + then: 'the json is rendered correctly' + result.json['bar'] == 'BAR' + result.json['name'] == 'Fred' } - void "Test the jsonapi render method when a domain instance defines an enum"() { - when:"rendering an object that defines an enum" + void 'Test the jsonapi render method when a domain instance defines an enum'() { + when: 'rendering an object that defines an enum' mappingContext.addPersistentEntity(EnumTest) - EnumTest enumTest = new EnumTest(name:"Fred", bar: TestEnum.BAR) + EnumTest enumTest = new EnumTest(name: 'Fred', bar: TestEnum.BAR) enumTest.id = 1 def result = render(''' -model { - Object object -} -json jsonapi.render(object) -''', [object: enumTest]) + model { + Object object + } + json jsonapi.render(object) + ''', [object: enumTest]) - then:"the json is rendered correctly" - result.jsonText == '''{"data":{"type":"enumTest","id":"1","attributes":{"name":"Fred","bar":"BAR"}},"links":{"self":"/enumTest/1"}}''' + then: 'the json is rendered correctly' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "data": { + "type": "enumTest", + "id": "1", + "attributes": { + "name": "Fred", + "bar": "BAR" + } + }, + "links": { + "self": "http://localhost:8080/enumTest/show/1" + } + } + ''') } } @@ -57,6 +81,4 @@ class EnumTest { TestEnum bar } -enum TestEnum { FOO, BAR} - - +enum TestEnum { FOO, BAR } \ No newline at end of file diff --git a/json/src/test/groovy/grails/plugin/json/view/IterableRenderSpec.groovy b/json/src/test/groovy/grails/plugin/json/view/IterableRenderSpec.groovy index 520b76ed8..c7f638876 100644 --- a/json/src/test/groovy/grails/plugin/json/view/IterableRenderSpec.groovy +++ b/json/src/test/groovy/grails/plugin/json/view/IterableRenderSpec.groovy @@ -1,173 +1,323 @@ package grails.plugin.json.view -import grails.plugin.json.view.test.JsonViewTest +import com.fasterxml.jackson.databind.ObjectMapper import grails.views.ViewException -import org.grails.testing.GrailsUnitTest +import grails.views.json.test.JsonViewUnitTest +import spock.lang.Shared import spock.lang.Specification -class IterableRenderSpec extends Specification implements JsonViewTest, GrailsUnitTest { +class IterableRenderSpec extends Specification implements JsonViewUnitTest { - void "Test render a collection type"() { - given:"A collection" - def players = [new Player(name:"Cantona")] + @Shared + ObjectMapper objectMapper - when:"A collection type is rendered" - def renderResult = render(''' -import groovy.transform.* -import grails.plugin.json.view.* - -@Field Collection players + def setupSpec() { + objectMapper = new ObjectMapper() + } -json g.render(players) -''', [players:players]) + void 'Test render a collection type'() { + given: 'A collection' + def players = [new Player(name: 'Cantona')] - then:"The result is an array" + when: 'A collection type is rendered' + def renderResult = render(''' + import groovy.transform.* + import grails.plugin.json.view.* + + @Field Collection players + + json g.render(players) + ''', [players: players]) + + then: 'The result is an array' renderResult.jsonText == '[{"name":"Cantona"}]' - } + void 'Test render a collection type with HAL'() { + given: 'A collection' + def players = [new Player(name: 'Cantona')] - void "Test render a collection type with HAL"() { - given:"A collection" - def players = [new Player(name:"Cantona")] - - when:"A collection type is rendered" + when: 'A collection type is rendered' def renderResult = render(''' -import groovy.transform.* -import grails.plugin.json.view.* - -@Field Collection players - -json hal.render(players) -''', [players:players]) - - then:"The result is an array" - renderResult.jsonText == '{"_links":{"self":{"href":"http://localhost:8080/player","hreflang":"en","type":"application/hal+json"}},"_embedded":[{"_links":{"self":{"href":"http://localhost:8080/player","hreflang":"en","type":"application/hal+json"}},"name":"Cantona"}]}' + import groovy.transform.* + import grails.plugin.json.view.* + + @Field Collection players + + json hal.render(players) + ''', [players: players]) + + then: 'The result is an array' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree(''' + { + "_links": { + "self": { + "href": "http://localhost:8080/player/index", + "hreflang": "en", + "type": "application/hal+json" + } + }, + "_embedded": [ + { + "_links": { + "self": { + "href": "http://localhost:8080/player/index", + "hreflang": "en", + "type": "application/hal+json" + } + }, + "name": "Cantona" + } + ] + } + ''') } - void "Test render a single element collection type with JSON API"() { - given: "A collection" + void 'Test render a single element collection type with JSON API'() { + given: 'A collection' mappingContext.addPersistentEntities(Player, Team) - Player player = new Player(name: "Cantona") + Player player = new Player(name: 'Cantona') player.id = 1 def players = [player] - when: "A collection type is rendered" + when: 'A collection type is rendered' def renderResult = render(''' -import groovy.transform.* -import grails.plugin.json.view.* - -@Field Collection players - -json jsonapi.render(players) -''', [players: players]) { - uri = "/foo" + import groovy.transform.* + import grails.plugin.json.view.* + + @Field Collection players + + json jsonapi.render(players) + ''', [players: players]) { + uri = '/foo' } - then: "The result is an array" - renderResult.jsonText == '{"data":[{"type":"player","id":"1","attributes":{"name":"Cantona"},"relationships":{"team":{"data":null}}}],"links":{"self":"/foo"}}' + then: 'The result is an array' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree(''' + { + "data": [ + { + "type": "player", + "id": "1", + "attributes": { + "name": "Cantona" + }, + "relationships": { + "team": { + "data": null + } + } + } + ], + "links": { + "self": "/foo" + } + } + ''') } - void "Test render a collection type with JSON API"() { - given: "A collection" + void 'Test render a collection type with JSON API'() { + given: 'A collection' mappingContext.addPersistentEntities(Player, Team) - Player player = new Player(name: "Cantona") + Player player = new Player(name: 'Cantona') player.id = 1 - Player player2 = new Player(name: "Louis") + Player player2 = new Player(name: 'Louis') player2.id = 2 def players = [player, player2] - when: "A collection type is rendered" + when: 'A collection type is rendered' def renderResult = render(''' -import groovy.transform.* -import grails.plugin.json.view.* - -@Field Collection players - -json jsonapi.render(players) -''', [players: players]) { - uri = "/foo" + import groovy.transform.* + import grails.plugin.json.view.* + + @Field Collection players + + json jsonapi.render(players) + ''', [players: players]) { + uri = '/foo' } - then: "The result is an array" - renderResult.jsonText == '{"data":[{"type":"player","id":"1","attributes":{"name":"Cantona"},"relationships":{"team":{"data":null}}},{"type":"player","id":"2","attributes":{"name":"Louis"},"relationships":{"team":{"data":null}}}],"links":{"self":"/foo"}}' + then: 'The result is an array' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree(''' + { + "data": [ + { + "type": "player", + "id": "1", + "attributes": { + "name": "Cantona" + }, + "relationships": { + "team": { + "data": null + } + } + }, + { + "type": "player", + "id": "2", + "attributes": { + "name": "Louis" + }, + "relationships": { + "team": { + "data": null + } + } + } + ], + "links": { + "self": "/foo" + } + } + ''') } - void "Test render a collection type with JSON API and pagination"() { - given: "A collection" + void 'Test render a collection type with JSON API and pagination'() { + given: 'A collection' mappingContext.addPersistentEntities(Player, Team) - Player player = new Player(name: "Cantona") + Player player = new Player(name: 'Cantona') player.id = 1 - Player player2 = new Player(name: "Louis") + Player player2 = new Player(name: 'Louis') player2.id = 2 def players = [player, player2] - when: "A collection type is rendered total must be greater than max (10)" + when: 'A collection type is rendered total must be greater than max (10)' def renderResult = render(''' -import groovy.transform.* -import grails.plugin.json.view.* - -@Field Collection players - -json jsonapi.render(players, [pagination: [resource: Player, total: 11]]) -''', [players: players], { - uri = "/foo" + import groovy.transform.* + import grails.plugin.json.view.* + + @Field Collection players + + json jsonapi.render(players, [pagination: [resource: Player, total: 11]]) + ''', [players: players], { + uri = '/foo' }) - then: "The result is an array" - renderResult.jsonText == '{"data":[{"type":"player","id":"1","attributes":{"name":"Cantona"},"relationships":{"team":{"data":null}}},{"type":"player","id":"2","attributes":{"name":"Louis"},"relationships":{"team":{"data":null}}}],"links":{"self":"/foo","first":"http://localhost:8080/player?offset=0&max=10","next":"http://localhost:8080/player?offset=10&max=10","last":"http://localhost:8080/player?offset=10&max=10"}}' + then: 'The result is an array' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree(''' + { + "data": [ + { + "type": "player", + "id": "1", + "attributes": { + "name": "Cantona" + }, + "relationships": { + "team": { + "data": null + } + } + }, + { + "type": "player", + "id": "2", + "attributes": { + "name": "Louis" + }, + "relationships": { + "team": { + "data": null + } + } + } + ], + "links": { + "self": "/foo", + "first": "http://localhost:8080/player/index?offset=0&max=10", + "next": "http://localhost:8080/player/index?offset=10&max=10", + "last": "http://localhost:8080/player/index?offset=10&max=10" + } + } + ''') } - void "Test render a collection type with JSON API and pagination override max"() { - given: "A collection" + void 'Test render a collection type with JSON API and pagination override max'() { + given: 'A collection' mappingContext.addPersistentEntities(Player, Team) - Player player = new Player(name: "Cantona") + Player player = new Player(name: 'Cantona') player.id = 1 - Player player2 = new Player(name: "Louis") + Player player2 = new Player(name: 'Louis') player2.id = 2 def players = [player, player2] - when: "A collection type is rendered total must be greater than max (10)" + when: 'A collection type is rendered total must be greater than max (10)' def renderResult = render(''' -import groovy.transform.* -import grails.plugin.json.view.* - -@Field Collection players - -json jsonapi.render(players, [pagination: [resource: Player, total: 11, max: 5]]) -''', [players: players]) { - uri = "/foo" + import groovy.transform.* + import grails.plugin.json.view.* + + @Field Collection players + + json jsonapi.render(players, [pagination: [resource: Player, total: 11, max: 5]]) + ''', [players: players]) { + uri = '/foo' } - then: "The result is an array" - renderResult.jsonText == '{"data":[{"type":"player","id":"1","attributes":{"name":"Cantona"},"relationships":{"team":{"data":null}}},{"type":"player","id":"2","attributes":{"name":"Louis"},"relationships":{"team":{"data":null}}}],"links":{"self":"/foo","first":"http://localhost:8080/player?offset=0&max=5","next":"http://localhost:8080/player?offset=5&max=5","last":"http://localhost:8080/player?offset=10&max=5"}}' + then: 'The result is an array' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree(''' + { + "data": [ + { + "type": "player", + "id": "1", + "attributes": { + "name": "Cantona" + }, + "relationships": { + "team": { + "data": null + } + } + }, + { + "type": "player", + "id": "2", + "attributes": { + "name": "Louis" + }, + "relationships": { + "team": { + "data": null + } + } + } + ], + "links": { + "self": "/foo", + "first": "http://localhost:8080/player/index?offset=0&max=5", + "next": "http://localhost:8080/player/index?offset=5&max=5", + "last": "http://localhost:8080/player/index?offset=10&max=5" + } + } + ''') } - void "Test render a collection type with JSON API and pagination (incorrect arguments)"() { - given: "A collection" + void 'Test render a collection type with JSON API and pagination (incorrect arguments)'() { + given: 'A collection' mappingContext.addPersistentEntities(Player, Team) - Player player = new Player(name: "Cantona") + Player player = new Player(name: 'Cantona') player.id = 1 - Player player2 = new Player(name: "Louis") + Player player2 = new Player(name: 'Louis') player2.id = 2 def players = [player, player2] - when: "A collection type is rendered total must be greater than max (10)" + when: 'A collection type is rendered total must be greater than max (10)' render(''' -import groovy.transform.* -import grails.plugin.json.view.* - -@Field Collection players - -json jsonapi.render(players, [pagination: [total: 11]]) -''', [players: players]) { - uri = "/foo" + import groovy.transform.* + import grails.plugin.json.view.* + + @Field Collection players + + json jsonapi.render(players, [pagination: [total: 11]]) + ''', [players: players]) { + uri = '/foo' } - then: "An illegal argument exception is thrown" + then: 'An illegal argument exception is thrown' def ex = thrown(ViewException) ex.cause instanceof IllegalArgumentException - ex.message == "Error rendering view: JSON API pagination arguments must contain resource and total" + ex.message == 'Error rendering view: JSON API pagination arguments must contain resource and total' } } diff --git a/settings.gradle b/settings.gradle index 571810f0c..9cd288510 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,30 +1,37 @@ plugins { - id 'com.gradle.enterprise' version '3.17' - id 'com.gradle.common-custom-user-data-gradle-plugin' version '2.0' + id 'com.gradle.develocity' version '3.18.1' + id 'com.gradle.common-custom-user-data-gradle-plugin' version '2.0.2' } -gradleEnterprise { +def isCI = System.getenv('CI') != null +def isLocal = !isCI +def isAuthenticated = System.getenv('DEVELOCITY_ACCESS_KEY') != null +def isBuildCacheAuthenticated = + System.getenv('DEVELOCITY_BUILD_CACHE_NODE_USER') != null && + System.getenv('DEVELOCITY_BUILD_CACHE_NODE_KEY') != null + +develocity { server = 'https://ge.grails.org' buildScan { - publishAlwaysIf(System.getenv('CI') == 'true') - publishIfAuthenticated() - uploadInBackground = System.getenv("CI") == null - capture { - taskInputFiles = true - } + publishing.onlyIf { isAuthenticated } + uploadInBackground = isLocal } } buildCache { - local { enabled = System.getenv('CI') != 'true' } - remote(gradleEnterprise.buildCache) { - def isAuthenticated = System.getenv('GRADLE_ENTERPRISE_ACCESS_KEY') - push = System.getenv('CI') == 'true' && isAuthenticated + local { enabled = isLocal } + remote(develocity.buildCache) { + push = isCI && isBuildCacheAuthenticated enabled = true + usernameAndPassword( + System.getenv('DEVELOCITY_BUILD_CACHE_NODE_USER') ?: '', + System.getenv('DEVELOCITY_BUILD_CACHE_NODE_KEY') ?: '' + ) } } rootProject.name = 'grails-views' + dependencyResolutionManagement { versionCatalogs { libs {