Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple DirectoryCodeResolver #191

Closed
Jerbell opened this issue Jan 4, 2023 · 15 comments
Closed

Multiple DirectoryCodeResolver #191

Jerbell opened this issue Jan 4, 2023 · 15 comments

Comments

@Jerbell
Copy link
Contributor

Jerbell commented Jan 4, 2023

I'm writing a "combined" app which uses JTEs from multiple source directories. There doesn't seem to be any way to do this at present.
As far as I can tell, I'd need to create a MultipleDirectoryCodeResolver, which would contain multiple DirectoryCodeResolvers and then have a way to differentiate which one to use based on the file name.

It's a bit weird. Does that sound like what you'd do?
Don't suppose this would be of interest to you at all?

@casid
Copy link
Owner

casid commented Jan 4, 2023

Oh, good question, I think it depends on what you'd like to achieve.

Are the multiple source directories independent and separate of each other? In this case you could create a template engine for each directory. We do this at work for web / mail / pdf templates, that use entirely different layouts and must not depend on each other.

Or, do you want to build a modular structure with templates in different directories, but still be able to reuse templates from other modules? This is a feature that's still missing in jte.

@Jerbell
Copy link
Contributor Author

Jerbell commented Jan 5, 2023

It's the difficult one - directories in different modules :)

This is what I've done so far:

import gg.jte.CodeResolver

class MultipleCodeResolver(
  private val defaultResolver: CodeResolver,
  private val resolverForNames: Map<CodeResolver, Set<String>>
) : CodeResolver {

  // inverse of resolverForNames
  private val nameToResolver: Map<String, CodeResolver>

  init {
    val allNames = resolverForNames.values.flatten().toSet()
    require(resolverForNames.values.sumOf { it.size } == allNames.size) { "at least one name is not unique" }

    nameToResolver = resolverForNames.entries.flatMap { (resolver, paths) -> paths.map { it to resolver } }.toMap()
  }

  override fun resolve(name: String): String = nameToResolver.getOrDefault(name, defaultResolver).resolve(name)

  override fun getLastModified(name: String) = nameToResolver.getOrDefault(name, defaultResolver).getLastModified(name)

  override fun resolveAllTemplateNames() =
    (resolverForNames.keys + defaultResolver).flatMap { it.resolveAllTemplateNames() }

  override fun exists(name: String) = nameToResolver.getOrDefault(name, defaultResolver).exists(name)
}

Not tested, but something like this should work.

@casid
Copy link
Owner

casid commented Jan 5, 2023

But do the modules call templates only within the module, or also templates from other modules? If it’s the first case you could go with option 1 and have a template engine per module.

If not, the ideal way would be to teach jte template modules, especially the IntelliJ plugin, so that it can help with auto complete.

@Jerbell
Copy link
Contributor Author

Jerbell commented Jan 5, 2023

I have Gradle module A including jte templates from modules B and C (if that answers your question). So you think this is a valid scenario and something you could add in?

@casid
Copy link
Owner

casid commented Jan 5, 2023

I’m not sure how easy this will be 😅 Does module A also have jte templates itself?

@Jerbell
Copy link
Contributor Author

Jerbell commented Jan 5, 2023

No - module A doesn't have templates of its own.
I still have my code/hack above which might work.
It's not a normal situation, though it might happen to others in the future?

@Jerbell
Copy link
Contributor Author

Jerbell commented Jan 5, 2023

Just to clarify - this is a single multi-module, multi-jar Gradle project. The new module will be like a mega-jar containing the other apps in some way.

@casid
Copy link
Owner

casid commented Jan 5, 2023

Hmm, in this case I would recommend to have a template engine for each module. Then you can include the jte precompiler gradle plugin to module B and C and expose the method to render a template for module A.

@Jerbell
Copy link
Contributor Author

Jerbell commented Jan 5, 2023

I'm using Javalin which can only accept one template engine (I believe).

@casid
Copy link
Owner

casid commented Jan 5, 2023

The Javalin jte plugin is just a very small amount of glue code around jte. It shouldn‘t be a problem to replace it with your custom logic that decides which module is used for template rendering.

@Jerbell
Copy link
Contributor Author

Jerbell commented Jan 5, 2023

OK - thanks for the tip. I'll have a look (tomorrow) and let you know how it goes.

@kelunik
Copy link
Collaborator

kelunik commented Jan 5, 2023

@casid It's something we should probably build, as we'll also need it at work sooner or later, I guess.

@Jerbell
Copy link
Contributor Author

Jerbell commented Jan 6, 2023

OK - I've done as you suggested. Same idea, but less code. I'll leave it up to you whether you close or turn into a proper issue. I'll follow and will use whatever you decide to do. 👍

@Jerbell
Copy link
Contributor Author

Jerbell commented Jan 6, 2023

code without the Util.ensureDependencyPresent checks

object MultiTemplateEngineJte : FileRenderer {

  private var defaultEngine: TemplateEngine? = null
  private var pathsToEngine: Map<String, TemplateEngine>? = null

  @JvmStatic
  fun configure(defaultEngine: TemplateEngine, enginesForFilePaths: Map<TemplateEngine, Set<String>>) {
    require(enginesForFilePaths.values.sumOf { it.size } == enginesForFilePaths.values.flatten().toSet().size) {
      "at least one file path is not unique"
    }

    this.defaultEngine = defaultEngine
    pathsToEngine = enginesForFilePaths.entries.flatMap { (engine, paths) -> paths.map { it to engine } }.toMap()
  }

  override fun render(filePath: String, model: Map<String, Any?>, ctx: Context): String {
    val stringOutput = StringOutput()
    pathsToEngine!!.getOrDefault(filePath, defaultEngine)!!.render(filePath, model, stringOutput)
    return stringOutput.toString()
  }
}

@casid
Copy link
Owner

casid commented Jan 8, 2023

Thanks @Jerbell! I think this is the best approach for this kind of problem.

@kelunik I agree, but let's do this properly once we have the situation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants