Skip to content

Instantly share code, notes, and snippets.

@trans
Created May 28, 2011 01:45
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Embed
What would you like to do?
Heart of Ruby As Meta-Language
require 'thread'
module RAML
def self.load(io, options={})
case io
when String
file = '(eval)'
code = io
when File
file = io.path
code = io.read
when IO
file = '(eval)'
code = io.read
end
options[:file] = file
RAML::EvalParser.new(options).parse!(code)
end
class EvalParser
#
SAFE = 4
# Need tainted data store.
HASH = {}.taint
#
attr :options
# You can pass in an object to act as the scope. All non-essential public
# and private methods will be removed from the scope unless a `keep`
# regex matches the name. Protected methods are also kept intact.
#
def initialize(options={})
@options = options
self.file = options[:file]
self.keep = options[:keep]
self.scope = options[:scope] || Object.new
self.multi_key = options[:multikey]
end
#
def parse!(code=nil, &block)
data = HASH.dup
scope.instance_variable_set("@__data__", data)
result = nil
if block
thread = Thread.new do
$SAFE = SAFE unless $SAFE == SAFE
result = scope.instance_eval(&block)
end
else
thread = Thread.new do
$SAFE = SAFE unless $SAFE == SAFE
result = scope.instance_eval(code, file)
end
end
thread.join
return result if data.empty? # good idea?
return data
end
# Returns Array<Regexp>.
attr_reader :keep
def keep=(list)
@keep = [list].compact.flatten
end
# Return String file name.
attr_accessor :file
# Returns Boolean.
def multi_key?
@multi_key
end
#
def multi_key=(bool)
@multi_key = !!bool
end
#
attr_reader :scope
#
def scope=(object)
@scope ||= (
qua_class = (class << object; self; end)
qua_class.__send__(:protected, :binding)
methods = [object.public_methods, object.private_methods].flatten
methods.each do |m|
next if /^(__|instance_|singleton_method_|method_missing$|extend$|initialize$|object_id$|p$)/ =~ m.to_s
next if keep.any?{ |k| k =~ m.to_s }
qua_class.__send__(:undef_method, m)
end
parser = self
object.instance_eval{ @__parser__ = parser }
object.extend MethodMissing
object
)
end
#
module MethodMissing
#
def method_missing(name, *args, &block)
return @__data__[name] if args.empty? and !block
if block
val = EvalParser.new(@__parser__.options).parse!(&block)
else
val = args.size == 1 ? args.first : args
end
if @__parser__.multi_key?
if @__data__.key?(name)
@__data__[name] = [@__data__[name]]
@__data__[name] << val
else
@__data__[name] = val
end
else
@__data__[name] = val
val
end
end
end
end
end

Ruby As Markup Language

Require the RAML library.

require 'raml'

Given a RAML document:

website "http://rubygems.org"

We can load the text via the #load method. (Note above document text has been placed in the @text variable.)

data = RAML.load(@text)

data.assert == {:website=>"http://rubygems.org"}

One of the nicer features of RAML derives from Ruby’s block notation, allowing for nested entries.

Given a RAML document:

resources do
  home "http://rubyworks.github.com/raml"
  docs "http://rubyworks.github.com/raml/docs/api"
  wiki "http://wiki.rubyworks.github.com/raml"
end

We get a two layer hash.

data = RAML.load(@text)

data[:resources][:home].assert == "http://rubyworks.github.com/raml"
data[:resources][:docs].assert == "http://rubyworks.github.com/raml/docs/api"
data[:resources][:wiki].assert == "http://wiki.rubyworks.github.com/raml"

RAML is also considers the content of a block. If it is a scalar entry, such as a String, then that will be assigned to the key.

Given a RAML document:

description do
  "This is a description.\n" \
  "It has multiple lines."
end

Loading this document,

data = RAML.load(@text)

data[:description].assert.start_with?("This is")
data[:description].assert.end_with?("does too.")

RAML has some options that makes it more flexible than many other data lanaguages. For instance, it can allow for multi-key entries.

Given a RAML document:

source "http://rubygems.org"
gem "facets", "~> 2.8"
gem "ansi", "~> 1.1"

We simply need to inform the loader to allow identical keys.

data = RAML.load(@text, :multikey=>true)

data.assert == {
  :source=>"http://rubygems.org",
  :gem=>[["facets", "~> 2.8"],["ansi", "~> 1.1"]]
}

If we did not turn on the multi-key option, then the last ‘gem` entry would have simply overwritten the former.

data = RAML.load(@text)

data.assert == {
  :source=>"http://rubygems.org",
  :gem=>["ansi", "~> 1.1"]
}
@RobertDober
Copy link

Hi Tom
first whack at #initialize

  ...
  result = nil

  thread = Thread.new co
     $SAFE = SAFE # Premature optimization is the root of all evil ;)
     result = if block
        scope.instance_eval( &block)
     else
        scope.instance_eval(code, file)
    end
  end

  thread.join

  return result if data.empty? # good idea? No idea, I have not understood the code fully yet (blush)
  return data

Cheers Robert

@trans
Copy link
Author

trans commented May 29, 2011

It was an IMPORTANT "Premature optimization" ;-) I made it selectable in the latest version (0 or 4). But it is important that it CAN run at safe level 4 --and that proved rather tricky.

[EDIT] Oops. I think I misunderstood you there. Did you mean I don't need the if $SAFE == SAFE part? Maybe so, I ran into a bug at one point where it appeared necessary, but maybe that's moot at this point. I'll have to try it out and see. Thanks.

The "good idea?" part refers to the example:

description do
  "This is a description.\n" \
  "It has multiple lines."
end

Since no methods were called in the block, the data hash isn't populated with anything. And if data hash is empty when the block finishes, it assumes the return value of the block must be the intended piece of data instead.

Thanks for looking at this. I am very curious about how it can be improved and what others think of it. I got the latest version up now at: https://github.com/rubyworks/raml/blob/master/lib/raml/eval_parser.rb

@trans
Copy link
Author

trans commented May 30, 2011

Thanks Robert for the improvement on the conditional. I've made the change on the repo.

@RobertDober
Copy link

RobertDober commented May 30, 2011 via email

@RobertDober
Copy link

RobertDober commented Nov 8, 2011 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment