Distelli/graphql-apigen

Create an stg template to generate a spring module

ryangardner opened this issue · 8 comments

With the latest Optional switches, it's almost really easy to use with spring.

The protected constructors make it a little tougher to work with spring than it would be if they were public. If the constructors on the type providers was switched to a public constructor, then a spring module that looks like this could be generated:

package com.foo.playground;

import com.distelli.posts.AuthorTypeProvider;
import com.distelli.posts.InputPostTypeProvider;
import com.distelli.posts.MutatePostsTypeProvider;
import com.distelli.posts.PostTypeProvider;
import com.distelli.posts.QueryPostsTypeProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class SpringConfig {
    
    @Bean
    public AuthorTypeProvider authorTypeProvider() {
        return new AuthorTypeProvider();
    }

    @Bean
    public MutatePostsTypeProvider mutatePostsTypeProvider() {
        return new MutatePostsTypeProvider();
    }

    @Bean
    public InputPostTypeProvider inputPostTypeProvider() {
        return new InputPostTypeProvider();
    }

    @Bean
    public PostTypeProvider postTypeProvider() {
        return new PostTypeProvider();
    }

    @Bean
    public QueryPostsTypeProvider queryPostsTypeProvider() {
        return new QueryPostsTypeProvider();
    }
    
}

Spring will automatically inject the dependencies for those type providers (the @Inject annotated Optional fields) and it will all just work.

I'm starting to look at how to get it to be generated. I've never used stringtemplate before so I'm kind of fumbling my way through this part of things based on what I already see in the stg file.

Any tips on how to implement this in the apigen would be appreciated

Interesting, I'm surprised that Spring doesn't use reflection to create a newInstance... how does spring work if you use constructor injection? I don't mind making the constructors public, but the protected is there to indicate that you shouldn't just "new" a new instance and expect it to work without DI.

Anyways, if your up for adding to the stg file, you actually need to change ApiGen.java first:

https://github.com/Distelli/graphql-apigen/blob/master/apigen/src/main/java/com/distelli/graphql/apigen/ApiGen.java#L203

Current the code is:

                    if ( stGroup.isDefined(generatorName + "GuiceModule") ) {
                        moduleBuilder.append(stGroup.getInstanceOf(generatorName+"GuiceModule")
                                             .add("model", model)
                                             .render());
                    }

That should probably change to this:

                    if ( guiceModuleName != null && stGroup.isDefined(generatorName + "GuiceModule") ) {
                        moduleBuilder.append(stGroup.getInstanceOf(generatorName+"GuiceModule")
                                             .add("model", model)
                                             .render());
                    } else if ( springModuleName != null && stGroup.isDefined(generatorName + "SpringModule") ) {
                        moduleBuilder.append(stGroup.getInstanceOf(generatorName+"SpringModule")
                                             .add("model", model)
                                             .render());
                    }

...of course you will need to add the springModuleName field which can be set via the ApiGen.Builder. You will also need to add a line to generate the guice module:

    if ( moduleBuilder.length() > 0 && guiceModuleName != null && stGroup.isDefined("guiceModule") ) {
          ...
    } else if ( moduleBuilder.length() > 0 && springModuleName != null && stGroup.isDefined("springModule") ) {
        ... basically a copy of the above code, but with springModule instead.
    }

Then in the stg file, you create a template name springModule that is similar to the current guiceModule template, and you do the same for the *GuiceModule templates so they are *SpringModule.

I hope this helps.

Thanks,
-Brian

I played around with it a bit more and it might be a lot simpler. Spring will make use of protected constructors if you are component-scanning or declaring the beans in an xml file, it's just tough to do it with a java-based configuration file unless they have a public constructor.

To mark a type to be picked up by package scanning, spring usually uses @Component or @Service but there is a JSR-330 @Named that spring will also use to pick up packages declared for package scanning.

So to get spring to support this as-is after adding the Optional<> stuff to this might be as simple as just adding @Named onto the type providers and then people using it in spring have to tell spring to package-scan the package that the code gets generated into and it all gets magically configured / picked up.

Would adding @Named onto those types do anything negative for people using Guice? If not, that might be a very easy way to get it to support both guice and spring by relying on the JSR-330 annotations.

Using guice in spring is not a terrible option, but I think I found a way to get Spring to pick it up even without an annotation on it at all. I can update the PR to update the documentation for the spring users - no code changes necessary.

Basically, just adding:

@ComponentScan(basePackages = "com.distelli.posts",
     includeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = javax.inject.Provider.class)
)

Spring will look for the Provider things and automatically create them...

Even better :)... although I would like to learn more about Spring so we can have a bit better support for it without requiring classpath scanning. Do you have any good online resources that I can read to find out about spring?

Thanks,
-Brian

The best source of docs is probably the official spring docs:

http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/

Package scanning and component scanning is a fairly common practice for spring applications.

You could probably generate a spring configuration class that manually registered the classes like this:

@Configuration
public class SpringConfiguration {
     @Autowired 
     protected ApplicationContext applicationContext;

    @PostConstruct
    public void registerTheGeneratedClasses() {
        applicationContext.register(SomeGeneratedProvider.class);
        applicationContext.register(AnotherGeneratedProvider.class);      
       ...
    }    
}

That would probably also work. I haven't tested it. It might save a a couple of ms at startup time.

(Especially with spring boot - which does most of its magic via classpath scanning and configuration based on what is in the classpath and what other beans you have already registered in the spring context)

Now that I've figured out a way to get a spring boot app to use the generated classes without needing to make any changes to them, things work pretty well.

I went ahead and merged in your pull request, would you like me to create another release with that change? I'd like to keep this issue open to see if we can generate a "SpringConfiguration" as you mention above. It might be a couple weeks out though since this is a project I'm working on in any "free time" I have.

Thanks,
-Brian

This change was released a while ago, so I'm closing this.