Does afBedSheet provide facets to tag classes as Services and methods as Route handlers?

StackOverflow https://stackoverflow.com/questions/22269866

  •  11-06-2023
  •  | 
  •  

Question

I'm playing with Fantom's afBedSheet framework and in its documentation here, the example goes...

using afIoc
using afBedSheet

class HelloPage {
  Text hello(Str name, Int iq := 666) {
    return Text.fromPlain("Hello! I'm $name and I have an IQ of $iq!")
  }
}

class AppModule {
  @Contribute { serviceType=Routes# }
  static Void contributeRoutes(OrderedConfig conf) {
    conf.add(Route(`/index`, Text.fromPlain("Welcome to BedSheet!")))
    conf.add(Route(`/hello/**`, HelloPage#hello))
  }
}
...

The contributeRoutes method above starts becoming hard to read and maintain when more and more Routes are added, especially when route handlers come from different classes.

I'm doing this differently: on each Service class I'm adding a static method that returns a list of Routes handled by its methods, like in this example:

using afBedSheet

class Info {

    static Route[] routes() {[
        Route(`/info`, #all),
        Route(`/info/pod`, #podAll),
        Route(`/info/pod/name`, #podName),
        Route(`/info/pod/version`, #podVersion),
    ]}

    Text all() {
        Text.fromJson(["This application blah blah blah"])
    }

    Text podAll() {
        pod := Pod.of(this)
        return Text.fromPlain("$pod.name $pod.version.toStr")
    }

    Text podName() {
        Text.fromPlain(Pod.of(this).name)
    }

    Text podVersion() {
        Text.fromPlain(Pod.of(this).version.toStr)
    }

}

Then my AppModule looks like this

using afIoc
using afBedSheet

class AppModule {

    @Contribute { serviceType=Routes# }
    static Void contributeRoutes(OrderedConfig conf) {
        Info.routes.each { conf.add(it) }
        AnotherService.routes.each { conf.add(it) }
        YetAnotherService.routes.each { conf.add(it) }
        ...
}

I'm trying to keep the AppModule clean and the Route definition and handler mapping closer to the implementing class. I expect this to make services/routes easier to maintain, but I'm not sure whether it is a good or bad idea. Benefits that I find doing this are

  • If I add a route handler method to a class, I declare the Route on the same class
  • Since route handler methods are part of the same class, I only have to type the slot name (e.g. #podVersion instead of Info#podVersion) which to me seems easier to read.

But as I said, I'm only playing with afBedSheet and I'd like to know from someone who has done a real production project with this framework if there is a good reason to declare the routes in the AppModule class, as the example shows.

Also, if what I'm doing is Ok or good, I'm wondering if there are (or if it would be a good idea to add) facets to change my Info class above to something more like:

using afBedSheet

@Service // Assuming there is such facet to let afBedSheet discover services
class Info {

    @Route { `/info` }  // <- Route handler facet
    Text all() {
        Text.fromJson(["This application blah blah blah"])
    }

    @Route { `/info/pod` }
    Text podAll() {
        pod := Pod.of(this)
        return Text.fromPlain("$pod.name $pod.version.toStr")
    }

    @Route { `/info/pod/name` }
    Text podName() {
        Text.fromPlain(Pod.of(this).name)
    }

    @Route { `/info/pod/version` }
    Text podVersion() {
        Text.fromPlain(Pod.of(this).version.toStr)
    }

}

If facets like these don't exist, I guess there must be good reasons to keep routes declaration in the AppModule and I'd like to know what they are.

Was it helpful?

Solution

In this (well worded question) I'm reading 2 distinct sub-questions:

  1. Is it okay to use static methods to declare BedSheet routes?
  2. Is it okay to create Route facets for BedSheet?

The common theme between the two questions is on-going clarity and maintenance. This can be quite a personal 'thing' and, like the proverbial cat, there is more than one way to skin it.

Anyhow, to address them individually:


Q). Is it okay to use static methods to declare BedSheet routes?

A). Yes, it is fine (but read on...)


Q). Is it okay to create Route facets for BedSheet?

A). In short, yes. In long...

As this (paraphrased) question implies - neither BedSheet nor IoC have any facets for declaring services or route handler methods. This is largely because it is not felt that such facets and associated services are 'core' enough to be included in the frameworks.

Keeping the configuration of routes and services in an AppModule means it is easy to find and keep track of - especially for newcomers to the code base.

On larger projects, a de-centralised configuration through facets can cause minor maintenance headaches. For if using facets, the only way to discover what services you have is through a textual search. The same applies to routes. A new comer trying to make sense of the project would have to wade through various pages of search results. Whereas a mere glance at an AppModule would herald the same understanding.

The word can is deliberately chosen because with enough coding diligence, be it class naming or directory structure, services and routes become logically grouped and are easy to find.

The Tales Framework has facets for declaring routes, but has this to say about them in Externalising Routes:

Defining routes along with methods as facets is cool and quick, but it has the following disadvantages:

  1. Does not clearly define the order in which routes will be picked up
  2. You cannot see all the routes that your app defines in one place.

So declaring services and routes with facets is not bad, just be wary of it. That said, I've been thinking about implementing a REST API based on facets, similar to JAX-RS (Java API for RESTful Services)!

As an aside, facet configuration is trivial to implement; for example if you had a facet called @Service that you placed on IoC service classes (or mixins) then you could just add the following line to your bind method:

const class AppModule {
    static Void bind(ServiceBinder binder) {
        AppModule#.pod.types .findAll { it.hasFacet(Service#) }.each { binder.bind(it) }
    }
}

To sum up:

If you are to be solely responsible for a code base, or are working on a smaller project, then using facets is fine. If maintenance is to shared by others, or if the project is non-trivial, then I'd consider keeping the configuration in either a single AppModule, or in multiple modules as defined by the @SubModule facet.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top