--[[ Semantic Model Module (API) Lair Configuration DSL Case Study H. Conrad Cunningham, Professor Computer and Information Science University of Mississippi This Lua module is based on the Ruby code for the Semantic Model used to support the configuration case study in: Martin Fowler, "One Lair and Twenty Ruby DSLs," Chapter 3, The ThoughtWorks Anthology: Essays on Software Technology and Innovation, The Pragmatic Bookshelf, 2008. Although Fowler's "lair" example is whimsical, it is representative of many practical configuration problems. The discussion of domain-specific language patterns is based on ideas from: Martin Fowler (with Rebecca Parsons). Domain-Specific Languages, Addison Wesley, 2011. For example, this is an example of the Semantic Model (159) from the Fowler book. Developed for CSci 658, Software Language Engineering, Fall 2013 1234567890123456789012345678901234567890123456789012345678901234567890 2013-10-10: Began Lair DSL project 2013-10-14: Completed Semantic Model module prototype adapted from Fowler's file lairs/model.rb Added leading _ to instance variables to avoid conflicts (Arose from ConfigurationBuilder clash on "uses") To-do: * Consider adding map, filter, and fold functions for the processing of tables as occurs in several situations herein. * Reconsider design of class_support module. Should the toString() and showValues() default definitions be deleted? redefined? --]] -- 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 -- Ruby-style symbols in Lua. These are just Lua strings, but they -- must be appropriate for use as Lua table field names (i.e., -- identifiers). This is adapted from a Lua Hack on user wiki. local S = setmetatable({},{ __index = function(t,k) return k end }) --[[ ABSTRACT CLASS Resource prototype This is not in Fowler's Ruby code, but it allows unification of some aspects of the implementation of its subclasses. --]] local Resource = makeClass(S.Resource) --[[ CONCRETE CLASS Notification prototype A Notification object buffers error messages. Internally, Notification uses a list-style table to store the messages. --]] local Notification = makeClass(S.Notification) function Notification:make() return Notification.new { _messages = {} } end function Notification:is_ok() return #self.messages == 0 end function Notification:fail(message) message = message or "uncommunicative failure" self._messages[#self._messages+1] = message end function Notification:error(message) self._messages[#self._messages+1] = message end function Notification:toString() -- overrides default if self:is_ok() then return "OK" end local result = {"Failed"} for _, m in ipairs(self._messages) do result[#result+1] = tostring(m) end return table.concat(result, "\n") end --[[ CONCRETE CLASS Configuration prototype A Configuration object holds a particular configuration for a lair. Internally, the object maintains a hashed table of Items. --]] local Configuration = makeClass(S.Configuration) function Configuration:make() return Configuration:new { _items = {} } end function Configuration:add_item(arg) self._items[arg:get_id()] = arg end function Configuration:get_item(id) return self._items[id] end function Configuration:all_items() local t = {} for _, it in pairs(self._items) do t[#t+1] = it end return t end function Configuration:toString() -- overrides default local t = {} for _, it in pairs(self:all_items()) do t[#t+1] = it:toString() end if #t == 0 then t[1] = "EMPTY" end return tostring(self:getTag()) .. "\n" .. table.concat(t,"\n") .. "\n" end --[[ CONCRETE CLASS Item prototype An Item object represents an item that can be in a lair's Configuration. An Item uses some Resources, provides other Resources, and depends upon other Items being present in the lair. Internally, an Item maintains those three relationships with three list-style tables: "uses", "provisions", and "dependencies". --]] local Item = makeClass(S.Item) function Item:make(id) return Item:new { _id = id, _uses = {}, _provisions = {}, _dependencies = {} } end function Item:add_usage(anItem) self._uses[#self._uses+1] = anItem end function Item:add_provision(anItem) self._provisions[#self._provisions+1] = anItem end function Item:add_dependency(anItem) self._dependencies[#self._dependencies+1] = anItem end function Item:get_id() return self._id end function Item:get_uses() return self._uses end function Item:get_provisions() return self._provisions end function Item:get_dependencies() return self._dependencies end function Item:toString() -- overrides default local ut = {} for _, u in ipairs(self:get_uses()) do ut[#ut+1] = u:toString() end local pt = {} for _, p in ipairs(self:get_provisions()) do pt[#pt+1] = p:toString() end local dt = {} for _, d in ipairs(self:get_dependencies()) do dt[#dt+1] = tostring(d:get_id()) end return tostring(self:getTag()) .. " " .. tostring(self:get_id()) .. "\nuses {" .. table.concat(ut, ", ") .. "}\nprovides {" .. table.concat(pt, ", ") .. "}\n => {" .. table.concat(dt, ", ") .. "}\n" end -- Not a method of Item, used by validate local function all_resources(list) for _, r in ipairs(list) do if not r:isInstanceOf(Resource) then return false end end return true end function Item:validate() local result = Notification:make() if not all_resources(self:get_uses()) then result:error("Bad resource in uses") end if not all_resources(self:get_provisions()) then result:error("Bad resource in provisions") end return result end function Item:check() assert(self:validate():is_ok(),"Item invalid") end --[[ CONCRETE CLASS Electricity prototype An Electricity resource object is an example of a simple Resource with a small, fixed number of properties that can be set by the constructor function. --]] local Electricity = makeClass(S.Electricity,Resource) function Electricity:make(power) return Electricity:new { _power = power } end function Electricity:get_power() return self._power end function Electricity:toString() -- overrides default return tostring(self:getTag()) .. "[" .. tostring(self:get_power()) .. "]" end --[[ CONCRETE CLASS Acid prototype (extends Resource) An Acid resource object is an example of a complex Resource (with potentially many properties) that requires multiple setter and getter functions for the property values. --]] local Acid = makeClass(S.Acid,Resource) function Acid:make() -- not needed, but included for consistency return Acid:new {} end function Acid:set_type(t) self._type = t end function Acid:set_grade(g) self._grade = g end function Acid:get_type() return self._type end function Acid:get_grade() return self._grade end function Acid:toString() -- overrides default return tostring(self:getTag()) .."[" .. tostring(self:get_type()) .. "," .. tostring(self:get_grade()) .. "]" end -- MODULE EXPORT return { Configuration = Configuration, Item = Item, Resource = Resource, Notification = Notification, Electricity = Electricity, Acid = Acid, S = S }