Prototyping language that transcompiles into pure Ruby code.
Ruby
Switch branches/tags
Nothing to show
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
examples
lib
Gemfile
LICENSE
README.md
Rakefile
VERSION
trope.gemspec

README.md

Trope

trope noun \ˈtrōp\ : A common or overused theme or device; cliché

Documentation - Gem - Source

Prototyping language that transcompiles into pure Ruby code.

  1. Build & refactor your concept in Trope.
  2. Transcompile into Ruby & destroy Trope files.
  3. Red, green, refactor.

Trope is not yet released.
The gem is just a placeholder.

Install

Bundler: gem 'trope'

RubyGems: gem install trope

Example

Create library.trope:

object Book
  attribute name -w String -!d 'Unnamed book'
  alias attribute name as title, label
  attribute isbn -w Integer
  attribute library -w Library do
    before write { @library.books.delete(self) unless @library.nil? }
    after  write { @library.books.push(self) unless @library.books.include?(self) }
  end
  initialize name, isbn
end

object Library
  attribute books -d []
  method add_book do |attributes_or_book <Hash, Book>|
    book = attributes_or_book.is_a?(Book) ? attributes_or_book : Book.new(attributes_or_book)
    book.library = self
    
    @books << book
  end
  alias method add_book as <<, push
end

Now generate the Ruby code:

$ trope compile libary.trope

Those few lines written in Trope will be transcompiled into the following pure Ruby code in library.rb:

class Book
  class Error < RuntimeError; end
  
  class ArgumentError < Error
    def initialize(range)
      @range = range
    end
    
    def to_s
      "wrong number of arguments (#{range.first} for #{range.last})"
    end
  end
  
  class InvalidAttributesError < Error
    def to_s
      'attributes must be a Hash or respond to #to_h'
    end
  end
  
  class MissingAttributeError < Error
    def initialize(attr_name, attr_class)
      @name, @class = attr_name.to_s, attr_class.to_s
    end
    
    def to_s
      "attribute '#@name' does not exist for #@class"
    end
  end
  
  class MissingNameError < Error
    def to_s
      'name cannot be nil'
    end
  end
  
  class InvalidNameError < Error
    def to_s
      'name must be an instance of String or respond to :to_s'
    end
  end
  
  class InvalidIsbnError < Error
    def to_s
      'isbn must be an instance of Integer or respond to :to_i'
    end
  end
  
  class MissingLibraryError < Error
    def to_s
      'library cannot be nil'
    end
  end
  
  class InvalidLibraryError < Error
    def to_s
      'library must be an instance of Library'
    end
  end
  
  attr_reader *(@@_attributes = [:name, :isbn, :library])
  
  def initialize(*arguments)
    attributes = arguments.last.is_a?(Hash) || arguments.last.respond_to?(:to_h) ? arguments.pop : {}
    attributes = attributes.to_h unless attributes.is_a?(Hash)
    
    raise ArgumentError, arguments.length..2 if arguments.length > 2
    
    default_attributes = {}
    default_attributes[:name] = arguments[0] if arguments.length > 0
    default_attributes[:isbn] = arguments[1] if arguments.length > 1
    attributes = default_attributes.merge(attributes)
    
    raise MissingNameError if attributes.has_key?(:name) && attributes[:name].nil?
    attributes[:name] = 'Unnamed book' unless attributes.has_key?(:name)
    
    attributes.each do |name, value|
      raise MissingAttributeError.new(name, self.class) unless @@_attributes.include?(name.to_sym)
      
      send("#{name}=", value)
    end
  end
  
  def name=(value)
    raise MissingNameError if value.nil?
    raise InvalidNameError unless value.is_a?(String) || value.respond_to?(:to_s)
    value = value.to_s unless value.is_a?(String)
    
    @name = value
  end
  
  def isbn=(value)
    raise InvalidIsbnError unless value.is_a?(Integer) || value.respond_to?(:to_i)
    value = value.to_i unless value.is_a?(Integer)
    
    @isbn = value
  end
  
  def library=(value)
    raise InvalidLibraryError unless value.is_a?(Library) || value.nil?
    
    @library.books.delete(self) unless @library.nil?
    
    @library = value
    
    @library.books.push(self) unless @library.books.include?(self)
    
    @library
  end
end

class Library
  class Error < RuntimeError; end
  
  class InvalidAttributesError < Error
    def to_s
      'attributes must be an instance of Hash or respond to #to_h'
    end
  end
  
  class MissingAttributeError < Error
    def initialize(attr_name, attr_class)
      @name, @class = attr_name.to_s, attr_class.to_s
    end
    
    def to_s
      "attribute '#@name' does not exist for #@class"
    end
  end
  
  class InvalidBooksError < Error
    def to_s
      'books must be an instance of Array or respond to #to_a'
    end
  end
  
  attr_reader *(@@_attributes = [:books])
  
  def initialize(attributes={})
    raise InvalidAttributesError unless attributes.is_a?(Hash) || attributes.respond_to?(:to_h)
    attributes = attributes.to_h unless attributes.is_a?(Hash)
    
    attributes[:books] = Array.new unless attributes.has_key?(:books)
    
    attributes.each do |name, value|
      raise MissingAttributeError.new(name, self.class) unless @@_attributes.include?(name.to_sym)
      
      send("#{name}=", value)
    end
  end
  
  def add_book(attributes_or_book={})
    raise InvalidAttributesError unless attributes_or_book.is_a?(Hash) || attributes_or_book.respond_to?(:to_h) || attributes_or_book.is_a?(Book)
    attributes_or_book = attributes_or_book.to_h unless attributes_or_book.is_a?(Hash) || attributes_or_book.is_a?(Book)
    
    book = attributes_or_book.is_a?(Book) ? attributes_or_book : Book.new(attributes_or_book)
    book.library = self
    
    @books << book
  end
  
  protected
  
  def books=(value)
    raise InvalidBooksError unless value.is_a?(Array) || value.respond_to?(:to_a)
    value = value.to_a unless value.is_a?(Array)
    
    @books = value
  end
end

Using the transcompiled Ruby code will produce the expected results:

p library = Library.new                     # => #<Library:0x00000 @books=[]>
p library.add_book name: 'Book 1', isbn: 1  # => [#<Book:0x00000 @name='Book 1', @isbn=1, @library=#<Library:0x00000 @books=[...]>>]
p library                                   # => #<Library:0x00000 @books=[#<Book:0x00000 @name='Book 1', @isbn=1, @library=#<Library:0x00000 ...>>]>
p library.books.first                       # => #<Book:0x00000 @name='Book 1', @isbn=1, @library=#<Library:0x00000 @books=[#<Book:0x00000 ...>]>>
p library.books.first.isbn = nil            # => nil
p library.books.first.name = nil            # => Book::MissingNameError: name cannot be nil
p library.books.first.library = nil         # => Book::MissingLibraryError: library cannot be nil
p library.books.first.isbn = ['array']      # => Book::InvalidIsbnError: isbn must be an instance of Integer or respond to :to_i
p library = Library.new(books: 123)         # => Library::InvalidBooksError: books must be an instance of Array or respond to #to_a

p Book.new('Book 2', 2)                            # => #<Book:0x00000 @name='Book 2', @isbn=2>
p Book.new('Book 3', isbn: 3)                      # => #<Book:0x00000 @name='Book 3', @isbn=3>
p Book.new(name: 'Book 4', isbn: 4)                # => #<Book:0x00000 @name='Book 4', @isbn=4>
p Book.new('Book #', 100, name: 'Book 5')          # => #<Book:0x00000 @name='Book 5', @isbn=100>
p Book.new('Book #', 100, isbn: 5)                 # => #<Book:0x00000 @name='Book #', @isbn=5>
p Book.new('Book #', 100, name: 'Book 5', isbn: 5) # => #<Book:0x00000 @name='Book 5', @isbn=5>

Keywords

Note: The root scope is the top-level namespace.

Object

Scope: root, attribute.
Description: This defines a class in Ruby.
Examples:

Define a Book class.

# trope

object Book
# ruby

class Book
  class Error < RuntimeError; end
end

Attribute

Scope: object.
Description: This defines a attribute reader/writer for an object.
Examples:

Define a name attribute for a Book class.

# trope

object Book
  attribute name
end
# ruby

class Book
  class Error < RuntimeError; end
  
  attr_reader *(@@_attributes = [:name])
  
  def initialize(attributes={})
    raise InvalidAttributesError unless attributes.is_a?(Hash) || attributes.respond_to?(:to_h)
    attributes = attributes.to_h unless attributes.is_a?(Hash)
    
    attributes.each do |name, value|
      raise MissingAttributeError.new(name, self.class) unless @@_attributes.include?(name.to_sym)
      
      send("#{name}=", value)
    end
  end
  
  protected
  
  def name=(value)
    @name = value
  end
end

Define a required name attribute for a Book class.

# trope

object Book
  attribute name -!
end
# ruby

class Book
  class Error < RuntimeError; end
  
  class MissingNameError < Error
    def to_s
      'name cannot be nil'
    end
  end
  
  attr_reader *(@@_attributes = [:name])
  
  def initialize(attributes={})
    raise InvalidAttributesError unless attributes.is_a?(Hash) || attributes.respond_to?(:to_h)
    attributes = attributes.to_h unless attributes.is_a?(Hash)
    
    attributes.each do |name, value|
      raise MissingAttributeError.new(name, self.class) unless @@_attributes.include?(name.to_sym)
      
      send("#{name}=", value)
    end
  end
  
  protected
  
  def name=(value)
    raise MissingNameError if value.nil?
    
    @name = value
  end
end

Define a name attribute with a writer for a Book class.

# trope

object Book
  attribute name -w
end
# ruby

class Book
  class Error < RuntimeError; end
  
  attr_reader *(@@_attributes = [:name])
  
  def initialize(attributes={})
    raise InvalidAttributesError unless attributes.is_a?(Hash) || attributes.respond_to?(:to_h)
    attributes = attributes.to_h unless attributes.is_a?(Hash)
    
    attributes.each do |name, value|
      raise MissingAttributeError.new(name, self.class) unless @@_attributes.include?(name.to_sym)
      
      send("#{name}=", value)
    end
  end
  
  def name=(value)
    @name = value
  end
end

The minus sign (-) indicates a 'switch' or 'option', must like most *nix command line programs. The example could also have been written like so:

object Book
  attribute name <String> -! -w -d 'Unnamed book'
end

The above examples will transcompile into the following:

class Book
  class Error < RuntimeError; end
  
  class InvalidAttributesError < Error
    def to_s
      'attributes must be a Hash or respond to #to_h'
    end
  end
  
  class MissingAttributeError < Error
    def initialize(attr_name, attr_class)
      @name, @class = attr_name.to_s, attr_class.to_s
    end
    
    def to_s
      "attribute '#@name' does not exist for #@class"
    end
  end
  
  class MissingNameError < Error
    def to_s
      'name cannot be nil'
    end
  end
  
  class InvalidNameError < Error
    def to_s
      'name must be an instance of String or respond to :to_s'
    end
  end
  
  attr_reader *(@@_attributes = [:name])
  
  def initialize(attributes={})
    raise InvalidAttributesError unless attributes.is_a?(Hash) || attributes.respond_to?(:to_h)
    attributes = attributes.to_h unless attributes.is_a?(Hash)
    
    raise MissingNameError if attributes.has_key?(:name) && attributes[:name].nil?
    
    attributes[:name] = 'Unnamed book' unless attributes.has_key?(:name)
    
    attributes.each do |name, value|
      raise MissingAttributeError.new(name, self.class) unless @@_attributes.include?(name.to_sym)
      
      setter_method = "#{name}="
      setter_method = "_#{setter_method}" unless self.class.method_defined?(setter_method)
      
      send(setter_method, value)
    end
  end
  
  def name=(value)
    raise MissingNameError if value.nil?
    raise InvalidNameError unless value.is_a?(String) || value.respond_to?(:to_s)
    value = value.to_i unless value.is_a?(Integer)
    
    @name = value
  end
end

Contributing

  • Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
  • Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
  • Fork the project
  • Start a feature/bugfix branch
  • Commit and push until you are happy with your contribution
  • Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
  • Please try not to mess with the Rakefile, VERSION, or Gemfile. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.

Copyright

Copyright © 2012 Ryan Scott Lewis ryan@rynet.us.

The MIT License (MIT) - See LICENSE for further details.