3

I am new to lua and I am trying to create a configuration DSL which allows to have sections that already have defaults.

So, the java table is predefined with lot of values

java = {
  source = 1.6,
  target = 1.6,
  directories = {
    sources = "src/main/java",
    output = "build/clases",
  },  
}

I have a Config prototype that implements __call so that when called as a function with a table constructor, it only overwrites the defaults. Something (like) this:

Config.__call = function(t, props)
  for k,v in pairs(props) do
    t[k] = v
  end
end

The idea is that you can call the dsl only to specify what you want to override:

java {
  source = 1.5,
  directories {
    sources = "customsrcdir",
  }
}

There is a Config.new method that allows to apply the prototype recursively to the tables so that all have a metatable with the __call method set.

My problem is with the "directories" subsection. It is evaluated in the global context, so the only way this works is:

java {
  source = 1.5,
    java.directories {
      sources = "customsrcdir",
  }
}

Which is pointless, as this is the same as doing:

java {
  source = 1.5
}

java.directories {
  sources = "customsrcdir",
}

I tried different approaches to have the desired DSL to work. One was setting a custom global environment with _ENV, but then I realized the table is evaluated before __call.

I wonder if someone with more lua experience has implemented a DSL like this using more advanced table/metatable/_ENV magic.

duncan
  • 6,113
  • 3
  • 29
  • 24
  • 1
    Would adding `=` just after `directories` hurt so much? You can't really say that "this is a function call" (you are of course aware of equality between `f ({...})` and `f {...}`), "but don't evaluate it just yet". Unless you do some crazy stuff with global state or something like that. – Bartek Banachewicz Dec 25 '13 at 11:33
  • Not sure I'm understanding your question fully. So you basically want `java.directories { "src/main/java2" }` to have the same effect as doing `java.directories = { "src/main/java2" }`? – greatwolf Dec 25 '13 at 11:40
  • Well if I use '=' it would overwrite the whole table and the default values. The idea of the function call is that I can add only what is passed, validate and ignore keys that are not part of the model. – duncan Dec 25 '13 at 12:04
  • So when you do `java { directories{"src/main/java2"} }` what's the semantically behavior you're looking for in your DSL? Should it override that field or does it add to that field? – greatwolf Dec 25 '13 at 12:16
  • directories is a bad example, but if directories had subkeys, I only want every mentioned key that is also in the originally initialized defaults, overwritten. I am not sure what is the key used when you specify it like {"src/main/java2"}. – duncan Dec 25 '13 at 12:26
  • I was using your original example above where `directories` table doesn't use key-index. Can you show another example that better illustrates the desired semantics? I assume there's some consistency in how you want it to work otherwise you would have to special case it for each field -- which could make it harder to maintain. – greatwolf Dec 25 '13 at 12:35

2 Answers2

1

It's possible to do it your way with calls, but the solution's so convoluted that it's not worth the omission of the =. If you still want the table merge/replacement functionality, then that's not too difficult.

local function merge(t1, t2)
  for k, v in pairs(t2) do
    -- Merge tables with tables, unless the replacing table is an array,
    -- in which case, the array table overwrites the destination.
    if type(t1[k]) == 'table' and type(v) == 'table' and #v == 0 then
      merge(t1[k], v)
    else
      t1[k] = v
    end
  end
end

local data = {
  java = {
    source = 1.6,
    target = 1.6,
    directories = {
      sources = "src/main/java",
      output = "build/classes",
    },
  }
}

local dsl = {}
load( [[
  java = {
    source = 1.5,
    directories = {
      sources = "customsrcdir",
    },
  }
]], 'dsl-config', 't', dsl)()

merge(data, dsl)

Dumping data will result in:

java = {
  directories = {
    output = "build/classes",
    sources = "customsrcdir"
  }
  source = 1.5,
  target = 1.6
}
Ryan Stein
  • 7,930
  • 3
  • 24
  • 38
  • I suppose this only works if java is the only top-level table, and you can't have multiple top-level sections right? – duncan Dec 25 '13 at 23:19
  • No, it works just fine with any number of top-level values, table or otherwise. Give it a try. It's just merging the `data` table with the environment table of the sandboxed script, and the sandboxed values take precedence, i.e. they overlap the old values. – Ryan Stein Dec 25 '13 at 23:24
  • Thanks, your solution is much closer to what I am looking for. However, it requires more effort once you start using functions in the configuration. Eg. try setting source = math.random(). It works very well for static configuration, but then what is the point of a DSL, I woul just load a yaml file or json. I was trying to get inspired by gradle here, that uses groovy closures, but those have this/that/owner and delegates, which makes all this easier. I guess, I will have to go with a different route for the DSL, but I am not sure if it will look very intuitive. – duncan Dec 26 '13 at 00:04
  • Replace the `local dsl = {}` line with `local dsl = setmetatable({}, {__index = _G})` if you want to import the usual globals. Just be aware that this limits the names of the top-level values you could use safely. I.e., `math = 5` will leave the actual math library inaccessible from that point onward, obviously. You might want to read up on the topic of [sandboxing](http://stackoverflow.com/q/1224708/828255) and [metamethods](http://www.lua.org/pil/13.4.html). – Ryan Stein Dec 26 '13 at 00:19
0

Check out how premake does it... Might be a more elegant solution than what you have going right now. http://en.wikipedia.org/wiki/Premake#Sample_Script

Nicolas Louis Guillemot
  • 1,570
  • 1
  • 10
  • 23