--[[ Builder for Expression Builder with Object Scoping (Builder17) 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 Expression Builder (343) and Object Scoping (385) DSL design pattern to implement the Lair Configuration DSL. We modified the design of his builder17 to include components of builder14 because of differences in the way local method calls are stated in Ruby and Lua. This DSL also uses the Method Chaining (373) and Nested Function (357) DSL design patterns. Developed for CSCI 658, Software Language Engineering, Fall 2013 1234567890123456789012345678901234567890123456789012345678901234567890 2013-10-15: Adapted from from builder14.lua and builder17.rb 2013-10-16: Added comment about Method Chaining and Nested Function patterns The Object Scoping pattern normally puts the DSL script in a subclass object and its implementation (an Expression Builder in this case) in the parent class object. The boilerplate header and trailer code likely make the loadstring call very FRAGILE--perhaps overly sensitive to formatting of the DSL script. A design that includes more explicit receivers to make connections might be better. For example, we could move ConfigurationBuilder from the header into the DSL script, use Resources for acid() and electricity() as in builder14, and have configuration store its results in a global variable to avoid the need for the explicit return. --]] -- 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 -- 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 class support module and define local names local cs = require "class_support" local makeClass = cs.makeClass -- Load builder module and define local names local eb = require "builder17" local ConfigurationBuilder = eb.ConfigurationBuilder local S = eb.S local PrimaryConfigurationRules = makeClass(S.PrimaryConfigurationRules,ConfigurationBuilder) function PrimaryConfigurationRules:make() return PrimaryConfigurationRules:new {} end function PrimaryConfigurationRules:run() -- acid() and electricity() not interpreted as method calls in Lua local parent = self:getSuper()[1] local function acid() return parent:acid() end local function electricity(arg) return parent:electricity(arg) end return ConfigurationBuilder: -- attaches to first item call ]] local trailer_code = [[ end -- end of run() method local pcr = PrimaryConfigurationRules:make() return pcr:run() ]] -- 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 result = dsl() return result end -- CONCRETE CLASS ItemBuilder prototype local ItemBuilder = makeClass(S.ItemBuilder) function ItemBuilder:make(parent,arg) return ItemBuilder:new { _parent = parent, _subject = Item:make(arg) } end function ItemBuilder:get_parent() return self._parent end function ItemBuilder:get_subject() return self._subject end function ItemBuilder:item(arg) return self:get_parent():item(arg) end function ItemBuilder:provides(arg) self:get_subject():add_provision(arg:get_subject()) return self end function ItemBuilder:uses(arg) self:get_subject():add_usage(arg:get_subject()) return self end function ItemBuilder:configuration() return self:get_parent():get_subject() end function ItemBuilder:depends_on(arg) self:get_subject(): add_dependency(self:configuration():get_item(arg)) return self end -- Define "showValues" method needed for default "toString" function ItemBuilder:showValues() return { self:get_parent():toString(), self:get_subject():toString() } end -- CONCRETE CLASS Resources prototype -- Different from "Resource" class in model local Resources = makeClass(S.Resources) function Resources:make() return Resources:new {} end -- Define "showValues" method needed for default "toString" function Resources:showValues() return { } end -- CONCRETE CLASS AcidBuilder prototype local AcidBuilder = makeClass(S.AcidBuilder) function AcidBuilder:make() return AcidBuilder:new { _subject = Acid:make() } end function AcidBuilder:get_subject() return self._subject end function AcidBuilder:type(arg) self:get_subject():set_type(arg) return self end function AcidBuilder:grade(arg) self:get_subject():set_grade(arg) return self end -- Define "showValues" method needed for default "toString" function AcidBuilder:showValues() return { self:get_subject():toString() } end -- CONCRETE CLASS ElectricityBuilder prototype local ElectricityBuilder = makeClass(S.ElectricityBuilder) function ElectricityBuilder:make(power) return ElectricityBuilder:new { _subject = Electricity:make(power) } end function ElectricityBuilder:get_subject() return self._subject end -- Define "showValues" method needed for default "toString" function ElectricityBuilder:showValues() return { self:get_subject():toString() } end -- CONCRETE CLASS ConfigurationBuilder prototype local ConfigurationBuilder = makeClass(S.ConfigurationBuilder) function ConfigurationBuilder:make() return ConfigurationBuilder:new { _subject = Configuration:make() } end function ConfigurationBuilder:get_subject() return self._subject end -- Fowler's Ruby code for builder14 had both a class and an instance -- method named "item". In the Lua class model used, there is no -- clear distinction between the two. So method "item" serves both -- roles. It is structured as an instance method, but it checks -- whether its first argument is the ConfigurationBuilder prototype -- object. If so, it behaves as a class method. Otherwise, it -- behaves as an instance method for an object of the class. -- Fowler's Ruby code for builder17 just had a method with the "else" -- leg below. But the Lua code needs to start with -- ConfigurationBuilder. function ConfigurationBuilder:item(arg) if self == ConfigurationBuilder then -- class method call local builder = ConfigurationBuilder:make() return builder:item(arg) else -- instance method call local result = ItemBuilder:make(self,arg) self:get_subject():add_item(result:get_subject()) return result -- returns ItemBuilder new ItemBuilder object end end function ConfigurationBuilder:electricity(power) return ElectricityBuilder:make(power) end function ConfigurationBuilder:acid() return AcidBuilder:make() end -- Define "showValues" method needed for default "toString" function ConfigurationBuilder:showValues() return { self:get_subject():toString() } end -- MODULE EXPORT return { execute_dsl = execute_dsl, ConfigurationBuilder = ConfigurationBuilder, ItemBuilder = ItemBuilder, Resources = Resources, ElectricityBuilder = ElectricityBuilder, AcidBuilder = AcidBuilder, S = S }