--[[ Builder for Literal Collection DSL (Builder22) Lair Configuration DSL Case Study H. Conrad Cunningham, Professor Computer and Information Science University of Mississippi See the comments in the Semantic Model Module. This Lua module is based on Fowler's builder program that uses a Literal Collection DSL design pattern to implement the Lair Configuration DSL. Developed for CSCI 658, Software Language Engineering, Fall 2013 1234567890123456789012345678901234567890123456789012345678901234567890 2013-10-15: Adapted from from Fowler's builder22.rb and builder14.lua 2013-10-16: Added get_config method Fowler's book Domain-Specific Languages gives two variations of the Literal Collection pattern--Literal List (417) and Literal Map (419) patterns. Since Lua's flexible tables are both lists (i.e., arrays or sequences) and maps (i.e., hashes), we mix the collections in this module. In a Literal Collection, we represent the DSL with a nested structure of lists or maps of literals to express the desired values and relationships. We can use this pattern effectively on the Lair Configuration given its nature; it fits Lua's capabilities well. --]] -- Load class support module local cs = require "class_support" -- Local names for functions in class_support module local makeClass, isInstanceOf, readOnly = cs.makeClass, cs.isInstanceOf, cs.readOnly -- Load semantic model module local sm = require "model" -- Define local names for convenience local Configuration, Item, S = sm.Configuration, sm.Item, sm.S local Resource, Electricity, Acid = sm.Resource, sm.Electricity, sm.Acid -- Configuration being built from DSL script local _config -- Function "get_config" returns the Configuration object built by the -- config() function. (This was not a feature of the Ruby code.) local function get_config() return _config end -- Function "oneOrMany" takes an object "obj" and a closure "clo". If -- "obj" is a table, then "oneOrMany" applies the closure to every -- element of the table's list portion. If "obj" is not a table, then -- "oneOrMany" applies the closure to the whole object. (Caveat: -- "oneOrMany" ignores the hash portion of a table, so it may not -- behave precisely like the Ruby version in such cases.) local function oneOrMany(obj, clo) if type(obj) == "table" then for _,v in ipairs(obj) do clo(v) end else clo(v) end end -- Function "oneOrManyTerms" takes an object "obj" and a closure -- "clo". If "obj" is table of tables, then "oneOrManyTerms" applies -- the closure to every element of the list portion of the outer -- table. If "obj" is anything else, then "oneOrMany" applies the -- closure to the whole object. (Caveat: "oneOrManyTerms" ignores the -- hash portion of a table, so it may not behave precisely like the -- Ruby version in some cases.) local function oneOrManyTerms(obj, clo) if type(obj) == "table" and type(obj[1]) == "table" then for _,v in ipairs(obj) do clo(v) end else clo(obj) end end -- Function "parse_resource" examines the resource description "r" and -- creates the needed semantic domain Resource object and populates -- its attributes. local function parse_resource(r) local result if type(r) == "table" then if r[1] == S.electricity then result = Electricity:make(r[2]) elseif r[1] == S.acid then result = Acid:make() result:set_type(r[2].type) result:set_grade(r[2].grade) else error("Invalid resource type: " .. tostring(r[1])) end else error("Invalid resource: " .. tostring(r)) end return result end -- Function "process_item_args" takes an semantic domain Item "anItem" -- and its whole description "hash", parses the attributes of the -- item, and inserts them into the Item object. local function process_item_args(anItem, args) for key, value in pairs(args) do if key == S.id then -- ignore elseif key == S.depends_on then oneOrMany(value, function(i) anItem:add_dependency(_config:get_item(i)) end) elseif key == S.uses then oneOrManyTerms(value, function(r) anItem:add_usage(parse_resource(r)) end) elseif key == S.provides then oneOrManyTerms(value, function(r) anItem:add_provision(parse_resource(r)) end) else error("Unexpected key " .. key,2) end end end -- Function "process_item" takes an item description "hash", creates -- the corresponding semantic domain Item object, populates it with -- its various attributes, and then inserts the new object into the -- current configuration "_config". The argument "hash" has required -- key "id" and several optional keys. local function process_item(hash) local newItem = Item:make(hash.id) process_item_args(newItem, hash) _config:add_item(newItem) return self end -- Function "config" takes the outermost literal collection "args" and -- creates the needed semantic domain elements and inserts them into -- the current configuration "_config". The outermost collection is a -- hash with having key "items". The element associated with this key -- is itself a list of item descriptions. local function config(args) if not _config then _config = Configuration:make() end for _,v in ipairs(args.items) do process_item(v) end end -- Variables "header_code" and "trailer_code" hold boilerplate code -- strings that are concatenated in front of and behind, respectively, -- the DSL script being executed by "execute_dsl". local header_code = [[ -- Load builder module and convenience named local lc = require "builder22" local S = lc.S return -- the Literal Collection table that follows ]] local trailer_code = "" -- Function "execute_dsl" loads a DSL script from a string, compiles -- it, executes it, and then returns the resulting Configuration -- object. local function execute_dsl(scriptFile) -- read dsl script file into string t local f = assert(io.open(scriptFile,"r")) local t = f:read("*a") f:close() -- combine with needed header, load, and execute script local script = header_code .. t .. trailer_code local dsl = loadstring(script) -- compiled in GLOBAL environment local data = dsl() _config = Configuration:make() config(data) return _config end return { execute_dsl = execute_dsl, config = config, get_config = get_config, S = S }