This is an automated email from the ASF dual-hosted git repository.

borinquenkid pushed a commit to branch test/document-datamapping-core-services
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 90c07523a7c860fc2a363edd81c8ae913867c6a8
Author: Walter Duque de Estrada <[email protected]>
AuthorDate: Sat Aug 15 19:02:09 2026 -0500

    Add coverage for update/delete-where/projection service implementers
    
    Covers DSL shapes with zero or partial coverage: UpdateOneImplementer,
    DeleteWhereImplementer, FindOneInterfaceProjectionWhereImplementer,
    FindAllPropertyProjectionImplementer, FindOnePropertyProjectionImplementer,
    the findById(id) shortcut in FindOneByImplementer, and the
    AbstractWriteOperationImplementer enhance() path for abstract-class
    services with pre-existing concrete write methods.
    
    Fixes a real bug found while writing the invalid-argument test for
    AbstractSaveImplementer: the error path referenced
    newMethodNode.declaringClass.module (null on the synthesized
    implementation method), crashing the compiler with a NullPointerException/
    GroovyBugError instead of reporting a clean compile error. Changed to
    abstractMethodNode.declaringClass.module.context, matching the pattern
    used elsewhere in this package (e.g. FindAllByImplementer).
    
    Co-Authored-By: Claude Sonnet 5 <[email protected]>
---
 .../implementers/AbstractSaveImplementer.groovy    |   4 +-
 .../services/ServiceImplementerEdgeCaseSpec.groovy | 364 +++++++++++++++++++++
 2 files changed, 366 insertions(+), 2 deletions(-)

diff --git 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractSaveImplementer.groovy
 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractSaveImplementer.groovy
index 2ba6864766..e53bdaea9c 100644
--- 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractSaveImplementer.groovy
+++ 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractSaveImplementer.groovy
@@ -64,8 +64,8 @@ abstract class AbstractSaveImplementer extends 
AbstractWriteOperationImplementer
                 argsExpression = varX(parameter)
             } else {
                 AstUtils.error(
-                        newMethodNode.declaringClass.module.context,
-                        newMethodNode,
+                        abstractMethodNode.declaringClass.module.context,
+                        abstractMethodNode,
                         "Cannot implement method for argument 
[${parameterName}]. No property exists on domain class [$domainClassNode.name]"
                 )
             }
diff --git 
a/grails-datamapping-core/src/test/groovy/grails/gorm/services/ServiceImplementerEdgeCaseSpec.groovy
 
b/grails-datamapping-core/src/test/groovy/grails/gorm/services/ServiceImplementerEdgeCaseSpec.groovy
new file mode 100644
index 0000000000..3941fa1016
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/grails/gorm/services/ServiceImplementerEdgeCaseSpec.groovy
@@ -0,0 +1,364 @@
+/*
+ *  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.
+ */
+package grails.gorm.services
+
+import org.codehaus.groovy.control.MultipleCompilationErrorsException
+import org.grails.datastore.gorm.services.Implemented
+import org.grails.datastore.gorm.services.implementers.DeleteWhereImplementer
+import 
org.grails.datastore.gorm.services.implementers.FindAllPropertyProjectionImplementer
+import org.grails.datastore.gorm.services.implementers.FindOneByImplementer
+import 
org.grails.datastore.gorm.services.implementers.FindOneInterfaceProjectionWhereImplementer
+import 
org.grails.datastore.gorm.services.implementers.FindOnePropertyProjectionImplementer
+import org.grails.datastore.gorm.services.implementers.UpdateOneImplementer
+import spock.lang.Specification
+
+/**
+ * Covers DSL shapes for {@link 
org.grails.datastore.gorm.services.implementers.ServiceImplementer}
+ * subclasses that are not otherwise exercised by {@link ServiceTransformSpec} 
or
+ * {@link WhereConnectionRoutingSpec}: update methods, {@code 
@Where}-annotated deletes,
+ * property projections and the {@code findById(id)} shortcut.
+ */
+class ServiceImplementerEdgeCaseSpec extends Specification {
+
+    void 'an update method with a matching id parameter is implemented via 
UpdateOneImplementer'() {
+        when:
+        Class service = new GroovyClassLoader().parseClass('''
+import grails.gorm.services.Service
+import grails.gorm.annotation.Entity
+
+@Service(Foo)
+interface FooService {
+    Foo updateFoo(Serializable id, String title)
+}
+@Entity
+class Foo {
+    String title
+}
+''')
+
+        then:
+        service.isInterface()
+
+        when:
+        Class impl = 
service.classLoader.loadClass("\$FooServiceImplementation")
+
+        then:
+        impl.getMethod('updateFoo', Serializable, 
String).getAnnotation(Implemented).by() == UpdateOneImplementer
+    }
+
+    void 'an update method with a Map args parameter is implemented via 
UpdateOneImplementer'() {
+        when:
+        Class service = new GroovyClassLoader().parseClass('''
+import grails.gorm.services.Service
+import grails.gorm.annotation.Entity
+
+@Service(Foo)
+interface FooService {
+    Foo updateFoo(Serializable id, Map args)
+}
+@Entity
+class Foo {
+    String title
+}
+''')
+
+        then:
+        service.isInterface()
+
+        when:
+        Class impl = 
service.classLoader.loadClass("\$FooServiceImplementation")
+
+        then:
+        impl.getMethod('updateFoo', Serializable, 
Map).getAnnotation(Implemented).by() == UpdateOneImplementer
+    }
+
+    void 'an update method on a service routed to a non-default connection 
saves through the instance API for that connection'() {
+        when:
+        Class service = new GroovyClassLoader().parseClass('''
+import grails.gorm.services.Service
+import grails.gorm.annotation.Entity
+import grails.gorm.transactions.Transactional
+
+@Service(Foo)
+@Transactional(connection = 'secondary')
+interface FooService {
+    Foo updateFoo(Serializable id, String title)
+}
+@Entity
+class Foo {
+    String title
+    static mapping = {
+        datasource 'secondary'
+    }
+}
+''')
+
+        then:
+        service.isInterface()
+
+        when:
+        Class impl = 
service.classLoader.loadClass("\$FooServiceImplementation")
+
+        then:
+        impl.getMethod('updateFoo', Serializable, 
String).getAnnotation(Implemented).by() == UpdateOneImplementer
+    }
+
+    void 'an update method with a parameter that matches no domain property 
fails to compile'() {
+        when:
+        new GroovyClassLoader().parseClass('''
+import grails.gorm.services.Service
+import grails.gorm.annotation.Entity
+
+@Service(Foo)
+interface FooService {
+    Foo updateFoo(Serializable id, String notAProperty)
+}
+@Entity
+class Foo {
+    String title
+}
+''')
+
+        then:
+        MultipleCompilationErrorsException e = 
thrown(MultipleCompilationErrorsException)
+        e.message.contains('Cannot implement method for argument 
[notAProperty]')
+    }
+
+    void 'a void @Where delete method is implemented via 
DeleteWhereImplementer'() {
+        when:
+        Class service = new GroovyClassLoader().parseClass('''
+import grails.gorm.services.Service
+import grails.gorm.services.Where
+import grails.gorm.annotation.Entity
+
+@Service(Foo)
+interface FooService {
+    @Where({ title ==~ pattern })
+    void deleteByTitle(String pattern)
+}
+@Entity
+class Foo {
+    String title
+}
+''')
+
+        then:
+        service.isInterface()
+
+        when:
+        Class impl = 
service.classLoader.loadClass("\$FooServiceImplementation")
+
+        then:
+        impl.getMethod('deleteByTitle', 
String).getAnnotation(Implemented).by() == DeleteWhereImplementer
+    }
+
+    void 'a Number-returning @Where delete method is implemented via 
DeleteWhereImplementer'() {
+        when:
+        Class service = new GroovyClassLoader().parseClass('''
+import grails.gorm.services.Service
+import grails.gorm.services.Where
+import grails.gorm.annotation.Entity
+
+@Service(Foo)
+interface FooService {
+    @Where({ title ==~ pattern })
+    Number deleteByTitle(String pattern)
+}
+@Entity
+class Foo {
+    String title
+}
+''')
+
+        then:
+        service.isInterface()
+
+        when:
+        Class impl = 
service.classLoader.loadClass("\$FooServiceImplementation")
+
+        then:
+        impl.getMethod('deleteByTitle', 
String).getAnnotation(Implemented).by() == DeleteWhereImplementer
+    }
+
+    void 'a @Where method returning an interface projection is implemented via 
FindOneInterfaceProjectionWhereImplementer'() {
+        when:
+        Class service = new GroovyClassLoader().parseClass('''
+import grails.gorm.services.Service
+import grails.gorm.services.Where
+import grails.gorm.annotation.Entity
+
+interface ITitle {
+    String getTitle()
+}
+
+@Service(Foo)
+interface FooService {
+    @Where({ title ==~ pattern })
+    ITitle findProjectionByTitle(String pattern)
+}
+@Entity
+class Foo {
+    String title
+}
+''')
+
+        then:
+        service.isInterface()
+
+        when:
+        Class impl = 
service.classLoader.loadClass("\$FooServiceImplementation")
+
+        then:
+        impl.getMethod('findProjectionByTitle', 
String).getAnnotation(Implemented).by() == 
FindOneInterfaceProjectionWhereImplementer
+    }
+
+    void 'a findAll<Domain><Property> method is implemented via 
FindAllPropertyProjectionImplementer'() {
+        when:
+        Class service = new GroovyClassLoader().parseClass('''
+import grails.gorm.services.Service
+import grails.gorm.annotation.Entity
+
+@Service(Foo)
+interface FooService {
+    List<String> findFooTitle()
+}
+@Entity
+class Foo {
+    String title
+}
+''')
+
+        then:
+        service.isInterface()
+
+        when:
+        Class impl = 
service.classLoader.loadClass("\$FooServiceImplementation")
+
+        then:
+        impl.getMethod('findFooTitle').getAnnotation(Implemented).by() == 
FindAllPropertyProjectionImplementer
+    }
+
+    void 'a findAll<Domain><Property> method returning an array is implemented 
via FindAllPropertyProjectionImplementer'() {
+        when:
+        Class service = new GroovyClassLoader().parseClass('''
+import grails.gorm.services.Service
+import grails.gorm.annotation.Entity
+
+@Service(Foo)
+interface FooService {
+    String[] findFooTitle()
+}
+@Entity
+class Foo {
+    String title
+}
+''')
+
+        then:
+        service.isInterface()
+
+        when:
+        Class impl = 
service.classLoader.loadClass("\$FooServiceImplementation")
+
+        then:
+        impl.getMethod('findFooTitle').getAnnotation(Implemented).by() == 
FindAllPropertyProjectionImplementer
+    }
+
+    void 'a find<Domain><Property> method returning a single value is 
implemented via FindOnePropertyProjectionImplementer'() {
+        when:
+        Class service = new GroovyClassLoader().parseClass('''
+import grails.gorm.services.Service
+import grails.gorm.annotation.Entity
+
+@Service(Foo)
+interface FooService {
+    String findFooTitle()
+}
+@Entity
+class Foo {
+    String title
+}
+''')
+
+        then:
+        service.isInterface()
+
+        when:
+        Class impl = 
service.classLoader.loadClass("\$FooServiceImplementation")
+
+        then:
+        impl.getMethod('findFooTitle').getAnnotation(Implemented).by() == 
FindOnePropertyProjectionImplementer
+    }
+
+    void 'a findById(Serializable) method is implemented via a direct get(id) 
call in FindOneByImplementer'() {
+        when:
+        Class service = new GroovyClassLoader().parseClass('''
+import grails.gorm.services.Service
+import grails.gorm.annotation.Entity
+
+@Service(Foo)
+interface FooService {
+    Foo findById(Serializable id)
+}
+@Entity
+class Foo {
+    String title
+}
+''')
+
+        then:
+        service.isInterface()
+
+        when:
+        Class impl = 
service.classLoader.loadClass("\$FooServiceImplementation")
+
+        then:
+        impl.getMethod('findById', 
Serializable).getAnnotation(Implemented).by() == FindOneByImplementer
+    }
+
+    void 'a pre-existing concrete write method on an abstract class service is 
enhanced with a default transaction'() {
+        when:
+        Class service = new GroovyClassLoader().parseClass('''
+import grails.gorm.services.Service
+import grails.gorm.annotation.Entity
+
+@Service(Foo)
+abstract class FooService {
+    Foo saveFoo(String title) {
+        Foo f = new Foo(title: title)
+        f.save(failOnError: true)
+        return f
+    }
+}
+@Entity
+class Foo {
+    String title
+}
+''')
+
+        then:
+        !service.isInterface()
+
+        when:
+        Class impl = 
service.classLoader.loadClass("\$FooServiceImplementation")
+
+        then:
+        impl.getMethod('saveFoo', 
String).getAnnotation(grails.gorm.transactions.Transactional) != null
+    }
+}

Reply via email to