Download - Writing your own DSL
Writing your own DSLYes, it is that easy!
Who am I?
● Rob Kinyon○ @rkinyon○ [email protected]
● Devops lead for many years● Developer in Ruby, Python, Perl, JS, and
others.
What is a DSL?
● Domain-Specific Language
What is a DSL?
● Domain-Specific Language● Language - A vehicle for communication
What is a DSL?
● Domain-Specific Language● Language - A vehicle for communication● Domain - A restrained set of concepts
What is a DSL?
● Domain-Specific Language● Language - A vehicle for communication● Domain - A restrained set of concepts● Specific - Limited to.
What is a DSL?
● Domain-Specific Language● Language - A vehicle for communication● Domain - A restrained set of concepts● Specific - Limited to.
○ No, really. :)
Language and Communication
● Communicate in one direction○ Author -> Executor
Language and Communication
● Communicate in two directions○ Author -> Executor○ Author -> Maintainer
Language and Communication
● Communicate in three directions○ Author -> Executor○ Author -> Maintainer○ Specifier -> Author
Language and Communication
● Communicate in four directions○ Author -> Executor○ Author -> Maintainer○ Specifier -> Author○ Author -> Verifier
Language and Communication
● Communicate in MANY directions○ Author -> Executor○ Author -> Maintainer○ Specifier -> Author○ Author -> Verifier○ Author -> Teammate(s)○ Developer -> Sysadmin/Devops○ … -> …
Language and Communication
● Communicate in MANY directions○ Author -> Executor○ Author -> Maintainer○ Specifier -> Author○ Author -> Verifier○ Author -> Teammate(s)○ Developer -> Sysadmin/Devops○ … -> …
The ONLY computer
Language and Communication
● Communicate in MANY directions○ Author -> Executor○ Author -> Maintainer○ Specifier -> Author○ Author -> Verifier○ Author -> Teammate(s)○ Developer -> Sysadmin/Devops○ … -> …
All humans
Language and Communication
● Communicate in MANY directions○ Author -> Executor○ Author <-> Maintainer○ Specifier <-> Author○ Author <-> Verifier○ Author <-> Teammate(s)○ Developer <-> Sysadmin/Devops○ … <-> …
All human communication is two-way
Domain-Specific
● Eskimos supposedly have 50+ words for “snow”○ Depends on how you count it
● Saami has 1000+ words dealing with reindeer○ snarri - a reindeer with short, branched horns○ busat - a bull with a single, large testicle
Domain-specific : Busat
Busat - The quality of having appropriately-specific expressiveness for the domain.
DSLs you already use
● SQL○ set manipulation DSL
● CSS○ tree-visitor-defining DSL for setting metadata
● HAML○ HTML-definition DSL
● Bash○ A crappy way of issue shell commands with logic
Places for a DSL
● Packaging and orchestration○ most devops/operations activities
● Configuration file generation○ web servers○ monitoring○ datastores
● Configuration value management across environments● Anything extremely complicated (such as SQL)● Anything repetitive (such as CSS)
Reasons for a DSL
● Let the important things shine● General-purpose is overly-verbose● Bugs hide in boilerplate● Non-developers can read and comprehend
○ And maybe even propose changes through PRs?
Reasons for a DSL
DSL is to Rubyas
Ruby is to Java
Writing a DSL
● Three passes○ Parsing○ Validation○ Production
Writing a DSL - Parsing
● DSL::Maker for parsing
Car = Struct.new(:make, :year, :engine)Engine = Struct.new(:hemi)
class VehicleDSL < DSL::Maker add_entrypoint(:car, { :make => String, :year => Integer, :engine => generate_dsl({ :hemi => Boolean, }) do Engine.new(hemi) end, }) do |*args| default(:make, args, 0) Car.new(make, model, engine) endend
car { make ‘Accord’ year 1990 engine { hemi Yes }}
car ‘Civic’ { year 2014}
Car = Struct.new(:make, :year, :engine)Engine = Struct.new(:hemi)
class VehicleDSL < DSL::Maker add_entrypoint(:car, { :make => String, :year => Integer, :engine => generate_dsl({ :hemi => Boolean, }) do Engine.new(hemi) end, }) do |*args| default(:make, args, 0) Car.new(make, model, engine) endend
car { make ‘Accord’ year 1990 engine { hemi Yes }}
car ‘Civic’ { year 2014}
Car = Struct.new(:make, :year, :engine)Engine = Struct.new(:hemi)
class VehicleDSL < DSL::Maker add_entrypoint(:car, { :make => String, :year => Integer, :engine => generate_dsl({ :hemi => Boolean, }) do Engine.new(hemi) end, }) do |*args| default(:make, args, 0) Car.new(make, model, engine) endend
car { make ‘Accord’ year 1990 engine { hemi Yes }}
car ‘Civic’ { year 2014}
Car = Struct.new(:make, :year, :engine)Engine = Struct.new(:hemi)
class VehicleDSL < DSL::Maker add_entrypoint(:car, { :make => String, :year => Integer, :engine => generate_dsl({ :hemi => Boolean, }) do Engine.new(hemi) end, }) do |*args| default(:make, args, 0) Car.new(make, model, engine) endend
car { make ‘Accord’ year 1990 engine { hemi Yes }}
car ‘Civic’ { year 2014}
Car = Struct.new(:make, :year, :engine)Engine = Struct.new(:hemi)
class VehicleDSL < DSL::Maker add_entrypoint(:car, { :make => String, :year => Integer, :engine => generate_dsl({ :hemi => Boolean, }) do Engine.new(hemi) end, }) do |*args| default(:make, args, 0) Car.new(make, model, engine) endend
car { make ‘Accord’ year 1990 engine { hemi Yes }}
car ‘Civic’ { year 2014}
Car = Struct.new(:make, :year, :engine)Engine = Struct.new(:hemi)
class VehicleDSL < DSL::Maker add_entrypoint(:car, { :make => String, :year => Integer, :engine => generate_dsl({ :hemi => Boolean, }) do Engine.new(hemi) end, }) do |*args| default(:make, args, 0) Car.new(make, model, engine) endend
car { make ‘Accord’ year 1990 engine { hemi Yes }}
car ‘Civic’ { year 2014}
#!/usr/bin/env ruby
require ‘vehicle/dsl’
filename = ARGV.shift || raise “No filename provided.”
vehicles = Vehicle::DSL.parse_dsl( IO.read(filename),)
# Do something here with vehicles
[ Car[ :make => ‘Accord’, :year => 1990, :engine => Engine[ :hemi => true, ], ], Car[ :make => Civic, :year => 2014, :engine => nil, ],]
. . .
truck ‘F-150’ { year 1999}
. . .
. . .Truck = Struct.new(:make, :year, :engine). . .class VehicleDSL < DSL::Maker . . . add_entrypoint(:truck, { :make => String, :year => Integer, :engine => . . ., }) do |*args| default(:make, args, 0) Truck.new(make, model, nil) endend
#!/usr/bin/env ruby
require ‘vehicle/dsl’
filename = ARGV.shift || raise “No filename provided.”
vehicles = Vehicle::DSL.parse_dsl( IO.read(filename),)
# Do something here with vehicles
[ . . . Truck[ :make => ‘F-150’, :year => 1999, :engine => nil ], . . .]
Writing a DSL - Validation
● DSL::Maker for parsing● DSL::Maker for validation
. . .
class VehicleDSL < DSL::Maker . . . add_validation(:car) do |car| unless car.engine return “Cars must have an engine” end endend
car { make ‘Accord’ year 1990 engine { hemi Yes }}
car ‘Civic’ { year 2014}
. . .
class VehicleDSL < DSL::Maker . . . add_validation(:car) do |car| unless car.engine return “Cars must have an engine” end endend
car { make ‘Accord’ year 1990 engine { hemi Yes }}
car ‘Civic’ { year 2014}
#!/usr/bin/env ruby
require ‘vehicle/dsl’
filename = ARGV.shift || raise “No filename provided.”
# This raises the errorvehicles = Vehicle::DSL.parse_dsl( IO.read(filename),)
# Do something here with vehicles
Error: Cars must have an engine
Writing a DSL - Production
● DSL::Maker for parsing● DSL::Maker for validation● You’re on your own for production
Writing a DSL - Production
● Work from outside in.○ Parsing is done inside-out.
● Transform in a series of passes.○ Expand everything (it’s just data)
● Don’t do anything irrevocable until the end○ Work in temp directories, stage everything
Conclusion
● DSL::Maker 0.1.0 is available right now● Patches welcome
○ 100% test coverage● I’m blogging about this at http:
//streamlined-book.blogspot.com○ First post on the topic
Questions?