rust-web / twig

Twig templating engine for Rust; work in progress.
http://rust-web.github.io/twig
Other
8 stars 1 forks source link

Extension Initialization #13

Open colin-kiegel opened 8 years ago

colin-kiegel commented 8 years ago

Ok, here is some complicated issue and the reason, why I think we need to have a "ExtensionsAreNotRegistered" error somewhere in our codebase.

I moved ExtensionRegistry to engine::Options.

pub struct Options {
    pub debug: bool,
   `// ...
    pub extensions: ExtensionRegistry,
}

Now Setup does this

        // init extensions
        try_chain!(self.options.extensions.init( /* foo */ );

The problem is with foo.

Lets look at ExtensionRegistry::init()

    pub fn init(&mut self, options: &/*mut?*/ Options) -> Result<(), ExtensionRegistryError> {
        if self.initialized {
            return err!(ExtensionRegistryErrorCode::AlreadyInitialized)
        }

        for (_, ext) in self.ext.iter() {
            ext.init(options);
   // ...

Notice the &mut self in init()

This loops over extensions implementing something like this

/// Extends the Twig Engine with new behaviour.
pub trait Extension : fmt::Debug {
    /// Initialize the engine.
    /// This is where you can load some file that contains filter functions for instance.
    fn init(&mut self, _options: &engine::Options) {} // TODO: add error handling ???
    // ...

>>> Uh oh. Now we have to fight the borrow checker. Why? (a) extensions are part of engine::Options (b) each extensions might want to initialize itself (via &mut self borrow). Due to (a) this implies a mutable borrow to engine::Options (c) It would be reasonable for extensions to read the options, but we can't have another borrow due to the mutable borrow of one extension.

How can we solve this? Safe or Unsafe?

Are you ok with this approach, or do you have a different suggestion?

Nercury commented 8 years ago

Separate Options and Extensions. When passing Options to extensions, maybe pass only the objects containing data that is needed. However, when extensions modify options, those are no longer the same Options. Therefore immutable reference to original options would suffice, provided that extensions create next incarnation of Options after initialization.

Nercury commented 8 years ago

OK, I am thinking of this "pipeline":

Worked for me! :)

P.S. Using unicorn from now when I don't care about name.

colin-kiegel commented 8 years ago

Ok, cool - thx - I will take some inspiration from codebase, too. E.g. I like the Config::from_hashmap() :-)

colin-kiegel commented 8 years ago

PS: I noticed a different approach in your codebase.

impl Extension for CoreExtension {
    fn apply(env: &mut Environment) {
        env.push_operators(vec![
            Operator::new_unary("not", 50, |_| unimplemented!()),
            Operator::new_unary("-", 500, |_| unimplemented!()),
            Operator::new_unary("+", 500, |_| unimplemented!()),

vs. my twigphp-oriented approach

/// Extends the Twig Engine with new behaviour.
pub trait Extension : fmt::Debug {
    /// Get the token parser instances to register with the engine.
    fn token_parsers(&self) -> HashMap<String, Box<TokenParser>> {
        HashMap::new()
    }

    /// Get the node visitor instances to register with the engine.
    fn node_visitors(&self) -> Vec<Box<NodeVisitor>> {
        Vec::new()
    }

I like the explicit style (alias "tell, don't ask") - I will change my pull request accordingly. :-)

Nercury commented 8 years ago

Allright, but have in mind that fn apply and Extension were just most minimal things that worked (haven't spend much time thinking about names).

The more interesting part I think is the Operator implementation. If you stumble upon function/Callable, don't pay much attention to Callable::Static, it's probably a bad way to do it.

But one idea might be useful: I am transforming all operator closures into functions that take arbitrary number of arguments (this one: Box<for<'e> Fn(&'e [Value]) -> RuntimeResult<Value>>), but keep argument count check hidden from extension developers here.

In the end I wished to leave only one kind of callable for any filter, operator or closure, which would simplify the execution, and allow to parse callables from other kinds of tokens.