Transitioning from Groovy to Spruce

Groovy is a dynamic language with static-typing and static compilation capabilities, for the Java platform. It integrates smoothly with any Java program and immediately delivers powerful features, including scripting capabilities, Domain-Specific Language authoring, runtime and compile-time meta-programming, and functional programming. Combined with Grails, a powerful web application framework, Groovy provides a productive full-stack development environment. Transitioning to Spruce, a TypeScript-based framework, involves adapting to a new ecosystem centered around JavaScript and TypeScript for full-stack web development.

Key Differences between Groovy and Spruce Development

Groovy/GrailsSpruce
Programming LanguageGroovyTypeScript
IDEIntelliJ IDEA, EclipseVisual Studio Code
App LifecycleGrails Controllers, ServicesSkillViewController lifecycle (optional AppViewController)
UI DesignGSP Templates, Grails ViewsHeartwood, ViewControllers
Event HandlingGrails Events, Spring EventsMercury
Data PersistenceGORMData Stores
Error HandlingTry-Catch BlocksTry-Catch Blocks, SpruceErrors
TestingSpock, JUnitTDD by the 3 laws
User AuthenticationSpring Security PluginMercury, Authenticator
User PermissionsSpring Security ACLMercury, Authorizer

Programming Language

Groovy

Groovy offers concise syntax, closures, and dynamic typing while maintaining Java compatibility. Classes and methods are simple and expressive.

class HelloController {
    def index() {
        def greeting = 'Hello, World!'
        def subtitle = 'This is a card'

        // Groovy closures
        def renderCard = { title, sub ->
            [title: title, subtitle: sub]
        }

        render(view: 'index', model: renderCard(greeting, subtitle))
    }
}

Spruce

Spruce is built entirely in TypeScript. This SkillViewController will render a full screen view with a CardViewController on it with a title and a subtitle. All ViewControllers (and SkillViewControllers) reduce down to a ViewModel that return from render(). In Spruce, 100% of the styling is handled by Heartwood (Storybook).

import {
  AbstractSkillViewController,
  CardViewController,
  ViewControllerOptions,
  buildSkillViewLayout,
  SkillView
} from '@sprucelabs/heartwood-view-controllers'

export default class RootSkillViewController extends AbstractSkillViewController {
  public static id = 'root'
  protected cardVc: CardViewController

  public constructor(options: ViewControllerOptions) {
    super(options)

    this.cardVc = this.Controller('card', {
      header: {
        title: 'Hello, World!',
        subtitle: 'This is a card'
      }
    })
  }

  public render(): SkillView {
    return buildSkillViewLayout('grid', {
      cards: [this.cardVc.render()]
    })
  }
}

IDE

Groovy in IntelliJ IDEA

IntelliJ IDEA provides excellent Groovy and Grails support with features like code completion, refactoring, and integrated Grails commands.

Spruce in Visual Studio Code

Spruce has been fully integrated into Visual Studio Code with custom extensions, launch configs, and settings.

App Lifecycle

Groovy/Grails

Grails manages the application lifecycle through controllers, services, and interceptors. Controllers handle requests while services contain business logic.

// grails-app/controllers/HelloController.groovy
class HelloController {

    // Injected service
    def helloService

    // Interceptor before action
    def beforeInterceptor = {
        log.info "Request received: ${actionName}"
    }

    def index() {
        def message = helloService.getMessage()
        [message: message]
    }

    def afterInterceptor = { model ->
        log.info "Response sent"
    }
}

// grails-app/services/HelloService.groovy
class HelloService {
    def getMessage() {
        return 'Hello from service!'
    }
}

Spruce

When a browser or native app loads your Skill, it will start by hitting its RootSkillViewController. If your Skill has an AppViewController declared, it will be loaded first. You can execute code at each stage by implementing a method by the name of the stage.

UI Design

Groovy/Grails

Grails uses GSP (Groovy Server Pages) for server-side rendering with tag libraries and layouts.

<!-- grails-app/views/layouts/main.gsp -->
<!DOCTYPE html>
<html>
<head>
    <title><g:layoutTitle default="My App"/></title>
    <g:layoutHead/>
</head>
<body>
    <g:layoutBody/>
</body>
</html>

<!-- grails-app/views/hello/index.gsp -->
<!DOCTYPE html>
<html>
<head>
    <meta name="layout" content="main"/>
</head>
<body>
    <div class="card">
        <h2>${title}</h2>
        <p>${subtitle}</p>
        <g:if test="${showButton}">
            <g:link action="submit" class="btn">Submit</g:link>
        </g:if>
    </div>
</body>
</html>

Spruce

Heartwood handles the rendering of all front end components. It adopts the philosphy of “Everything Beautiful”. While you are constrained to the views that Heartwood provides, you can customize their look by running the following in your skill:

spruce create.theme

This will create a skill.theme.ts file you can customize. If you want to apply a theme to your organization (vs just your skill), you can utilize the Theme Skill.

Event Handling

Groovy/Grails

Grails provides event handling through the Events plugin and Spring’s event system for decoupled communication.

// Publishing events
import grails.events.Events

class FeedbackService implements Events {

    def submitFeedback(String feedback) {
        // Process feedback
        saveFeedback(feedback)

        // Publish event
        notify('feedback.submitted', feedback)
    }
}

// Listening to events
import grails.events.annotation.Subscriber

class NotificationService {

    @Subscriber('feedback.submitted')
    def onFeedbackSubmitted(String feedback) {
        log.info "Feedback received: ${feedback}"
        // Send notification, etc.
    }
}

// Using Spring events
import org.springframework.context.event.EventListener

class FeedbackEvent {
    String feedback
}

@EventListener
void handleFeedback(FeedbackEvent event) {
    println "Handling feedback: ${event.feedback}"
}

Spruce

In Spruce, your views are rendered on the edge, while your Skill is hosted on a server. So, you have to use the Mercury event system to communicate between the two. Mercury also allows you to pass information other skills.


// inside of Skill View sending message to the Skill with the namespace "eightbitstories"

const client = await this.connectToApi()
await this.client.emitAndFlattenResponses(
  'eightbitstories.submit-feedback::v2023_09_05',
  {
    payload: {
      feedback: 'Help make this better!',
    },
  }
)

Data Persistence

Groovy/Grails

GORM (Grails Object Relational Mapping) provides a powerful, expressive way to interact with databases using domain classes.

// grails-app/domain/Car.groovy
class Car {
    String make
    String model
    Integer year

    static constraints = {
        make blank: false, maxSize: 100
        model blank: false, maxSize: 100
        year min: 1900, max: 2100
    }

    static mapping = {
        table 'cars'
        sort year: 'desc'
    }

    String toString() {
        "${year} ${make} ${model}"
    }
}

// Usage in a service or controller
class CarService {
    def createCar() {
        def car = new Car(make: 'Toyota', model: 'Camry', year: 2022)
        car.save(flush: true)
    }

    def findCars() {
        // Dynamic finders
        def toyotas = Car.findAllByMake('Toyota')

        // Criteria queries
        def recentCars = Car.createCriteria().list {
            ge('year', 2020)
            order('year', 'desc')
        }

        // Where queries
        def camrys = Car.where {
            make == 'Toyota' && model == 'Camry'
        }.list()

        return recentCars
    }
}

Spruce

In Spruce, you’ll use the Stores feature to persist data. The stores use Schemas to define the shape of the data.

spruce create.store

Once you configure your store, you can use it in your skill’s event listener like this:

export default async (
  event: SpruceEvent<SkillEventContract, EmitPayload>
): SpruceEventResponse<ResponsePayload> => {
  const { stores } = event

  const cars = await stores.getStore('cars')
  await cars.createOne({
    make: 'Toyota',
    model: 'Camry',
    year: 2022
  })

  return {
    success: true,
  }
}

Error Handling

Groovy/Grails

Groovy uses try-catch blocks and Grails provides exception handling through error controllers and custom exceptions.

// Custom exception
class CarNotFoundException extends RuntimeException {
    CarNotFoundException(String message) {
        super(message)
    }
}

// Service with error handling
class CarService {
    def getCar(Long id) {
        try {
            def car = Car.get(id)
            if (!car) {
                throw new CarNotFoundException("Car with id ${id} not found")
            }
            return car
        } catch (CarNotFoundException e) {
            log.error e.message
            throw e
        } catch (Exception e) {
            log.error "Unexpected error: ${e.message}"
            throw new RuntimeException("Failed to retrieve car", e)
        }
    }
}

// Error controller (grails-app/controllers/ErrorController.groovy)
class ErrorController {
    def handle404() {
        render(view: '/errors/notFound')
    }

    def handle500() {
        render(view: '/errors/serverError')
    }
}

Spruce

Spruce provides a much more robust, standardized error handling system. You can use the SpruceError class to create custom errors, you define the Schemas for those errors to give them shape, and then use try-catch blocks to handle them.

spruce create.error

This will create an error builder inside of your skill at ./src/errors/{{errorName}}.builder.ts. Inside there is the schema that defines your error.

You can throw an error you have defined like this:

throw new SpruceError({
  code: 'MY_ERRORS_NAME_HERE',
  friendlyMessage: 'All errors can provide a friendly error message!',
})

Testing

Groovy/Grails

Spock is the preferred testing framework for Groovy, offering expressive BDD-style tests with powerful mocking capabilities.

import spock.lang.Specification

class CarServiceSpec extends Specification {

    def carService = new CarService()

    def "should create a new car"() {
        given: "car details"
        def make = 'Toyota'
        def model = 'Camry'
        def year = 2022

        when: "creating the car"
        def car = carService.createCar(make, model, year)

        then: "car is saved with correct values"
        car.id != null
        car.make == make
        car.model == model
        car.year == year
    }

    def "should throw exception when car not found"() {
        when: "fetching non-existent car"
        carService.getCar(999L)

        then: "exception is thrown"
        thrown(CarNotFoundException)
    }

    def "should find cars by make"() {
        given: "multiple cars exist"
        carService.createCar('Toyota', 'Camry', 2022)
        carService.createCar('Honda', 'Accord', 2022)

        expect: "filtering by make works"
        carService.findByMake('Toyota').size() == 1
    }
}

Spruce

Everything in Spruce starts with a Test. If you want to write a piece of production code, you must start with a failing test.

spruce create.test

Once your test file is created, you are ready to start!

User Authentication

Groovy/Grails

The Spring Security plugin provides comprehensive authentication support for Grails applications.

// build.gradle
dependencies {
    implementation 'org.grails.plugins:spring-security-core:5.0.0'
}

// grails-app/domain/User.groovy
class User {
    String username
    String password
    boolean enabled = true

    static constraints = {
        username blank: false, unique: true
        password blank: false
    }

    Set<Role> getAuthorities() {
        UserRole.findAllByUser(this)*.role
    }
}

// Controller with security
import grails.plugin.springsecurity.annotation.Secured

class DashboardController {

    def springSecurityService

    @Secured(['ROLE_USER'])
    def index() {
        def currentUser = springSecurityService.currentUser
        [user: currentUser]
    }

    @Secured(['IS_AUTHENTICATED_ANONYMOUSLY'])
    def publicPage() {
        render 'This is public'
    }
}

// Checking authentication in service
class ProfileService {
    def springSecurityService

    def getCurrentUserProfile() {
        if (springSecurityService.isLoggedIn()) {
            def user = springSecurityService.currentUser
            return Profile.findByUser(user)
        }
        return null
    }

    def logout() {
        springSecurityService.logout()
    }
}

Spruce

Because Mercury handles user authentication (and authorization). You can use the Authenticator to know if a person is logged in or not. You can also use it to log a person in or out.

//inside your Skill View's load lifecycle method
public async load(options: SkillViewControllerLoadOptions) {
  const { authenticator } = options

  this.log.info(authenticator.isLoggedIn())
  this.log.info(authenticator.getPerson())

  // force person to be logged out
  authenticator.clearSession()

}

User Permissions

Groovy/Grails

Spring Security provides role-based and ACL-based permission systems for fine-grained access control.

// Role-based permissions
import grails.plugin.springsecurity.annotation.Secured

class StoryController {

    @Secured(['ROLE_AUTHOR'])
    def create() {
        // Only authors can create stories
    }

    @Secured(['ROLE_ADMIN', 'ROLE_AUTHOR'])
    def edit(Long id) {
        // Admins and authors can edit
    }
}

// Checking permissions programmatically
import grails.plugin.springsecurity.SpringSecurityService

class StoryService {

    SpringSecurityService springSecurityService

    def canGenerateStory() {
        def authorities = springSecurityService.principal?.authorities*.authority
        return authorities?.contains('ROLE_STORY_GENERATOR')
    }

    def generateStory() {
        if (!canGenerateStory()) {
            throw new AccessDeniedException('Permission denied')
        }
        // Generate story logic
    }
}

// Using Spring Security expressions
@PreAuthorize("hasRole('ROLE_ADMIN') or #story.author == authentication.name")
def updateStory(Story story) {
    // Only admin or the story author can update
}

Spruce

Mercury also handles all your Permission needs. To introduce new permissions into the platform, you need to create a Permission Contract in your skill:

spruce create.permissions

Then you can do permission checks in your Skill View like this:

//inside your Skill View's load lifecycle method
public async load(options: SkillViewControllerLoadOptions) {
  const { authorizer } = options

  const permissions = await authorizer.can({
    contractId: 'eightbitstories.eight-bit-stories',
    permissionIds: ['can-generate-story'],
  })

  const canGenerateStory = permissions['can-generate-story']

}

Something Missing?

Request Documentation Enhancement

Now What?

Install the Development Theatre
It looks like you are using Internet Explorer. While the basic content is available, this is no longer a supported browser by the manufacturer, and no attention is being given to having IE work well here.