--[[ Builder for Dynamic Reception DSL (Builder23) 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 the Dynamic Reception DSL design pattern to implement the Lair Configuration DSL. Developed for CSCI 658, Software Language Engineering, Fall 2013 1234567890123456789012345678901234567890123456789012345678901234567890 2013-10-17: Adapted from Fowler's builder23.rb, etc. --]] -- Load class support module local cs = require "class_support2" -- 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 -- Utility functions local function keys(hash) local keys = {} for k,_ in pairs(hash) do keys[#keys+1] = k end return keys end local function values(hash) local values = {} for _,v in pairs(hash) do values[#values+1] = v end return values end local function capitalize(s) return string.upper(string.sub(s,1,1)) .. string.sub(s,2) end -- Add to class Configuration function Configuration:add_resource_type(name, arg) self._resource_types = self._resource_types or {} self._resource_types.name = arg end function Configuration:resource_types() return values(self._resource_types) end function Configuration:resource_names() return keys(self._resource_types) end function Configuration:show_resources() for _,v in pairs(self._resource_types) do print(v) end end function Configuration:resource_type(name) return self._resource_types.name end function Configuration:convert_resources() for _,it in pairs(self._items) do it:convert_resources() end end -- Add to class Item function Item:convert_resource(arg) -- In both Ruby and Lua, this method seems lame! -- Why do things dyamically if names are hardcoded? local className = capitalize(tostring(arg.id)) print(className) if arg.id == S.electricity then return Electricity:make(arg.power) elseif arg.id == S.acid then local a = Acid:make() a.grade = arg.grade a.type = arg.type return a else error("Unknown resource class " .. tostring(arg.name)) end end function Item:convert_resources() local ps = {} for _,p in ipairs(self._provisions) do ps[#ps+1] = self:convert_resource(p) end self._provisions = p local us = {} for _,u in ipairs(self._uses) do us[#us+1] = self:convert_resource(u) end self._uses = us end -- CONCRETE CLASS RulesBuilder prototype local RulesBuilder = makeClass(S.RulesBuilder) function RulesBuilder:make() return RulesBuilder:new { _configuration = Configuration.make() } end function RulesBuilder:get_configuration() return _configuration end function RulesBuilder:item(srcSymbol) _current_item = Item:make(srcSymbol) _configuration:add_item(_current_item) return self end function RulesBuilder:uses(arg) _current_item:add_usage(arg) return self end function RulesBuilder:provides(arg) _current_item:add_provision(arg) return self end function RulesBuilder:depends_on(arg) _current_item:add_dependency(_configuration:get_item(arg)) return self -- not in Ruby code, but seems needed for consistency end function RulesBuilder:resource(name, ...) attributes[#attributes+1] = name local new_resource = {...} _configuration:add_resource_type(name, new_resource) end function RulesBuilder:keyMissing(sym,...) super sym, *args unless _configuration:resource_names:include? sym obj = _configuration:resource_type(sym):new obj[:name] = sym args[0]:each_pair do |key, value| obj[key] = value end return obj 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". This code may be -- quite sensitive to the formatting of the DSL script file. local header_code = [[ -- Load builder module and define local names local dr = require "builder23" local RulesBuilder = dr.ConfigurationBuilder local S = dr.S print("DEBUG in header code dr = " .. tostring(dr)) ]] 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 print(script) local dsl = loadstring(script) -- compiled in GLOBAL environment local result = dsl() return RulesBuilder:get_configuration() end -- MODULE EXPORT return { execute_dsl = execute_dsl, RulesBuilder = RulesBuilder, S = S }