Dr Nic

53 cheat sheets and growing

I said previously that errtheblog’s cheat app would have 100s+ of cheat sheets.

After a month and a half, they are now half way. There is still more cheating to be done.

> require 'map_by_method'
> require 'hpricot'
> require 'open-uri'
> doc = Hpricot(open("http://cheat.errtheblog.com/b"))
> cheatsheets = doc.search('div.content/ul/li').children.map_inner_html.flatten
=> ["ascii", "assertions", "assert_select", "balloon", "bash", "belongs_to", "blogs",
"callbacks", "capistrano", "cheat", "database_yml", "deprecated", "environments",
"exceptions", "firebug", "google_mail", "google_reader", "gruff", "has_and_belongs_to_many",
"has_many", "has_one", "http_status_codes", "iomodes", "jedit_ruby_editor_plugin",
"link_to_remote", "markaby", "markdown", "matz_bot", "microformats_helper",
"migrations", "mocha", "nonsense", "prototype", "rails_console", "rails_edge", "rdoc",
"readline", "regex", "rjs", "rspec", "ruby", "scrapi", "sprintf", "sti", "strftime", "svn",
"tempfile", "textmate_rails", "tld", "validations", "vim", "vim_goodies", "wmii3"]
> puts "#{cheatsheets.length} cheat sheets and counting..."
53 cheat sheets and counting...

By the way, they celebrated when they hit 42.

[ANN] Generating new gems for graceful goodliness

Chicken!

I don’t like you [1]. You don’t share code. I know, I know, you’ve had good reasons – you don’t know how to create a Ruby gem, how to upload it to a gem server like RubyForge, and you’re a chicken. Today we’ll remove the first of these minor roadblocks, with a New Gem Generator!

Now you can take any library or Rails plugin or command line application, gemify it, and easily share it with the Ruby world. With gems you get in-built version support (you can specify which version of a gem you want when you use it via the require_gem method), an encapsulated, consistent folder structure for your bin/lib/test folders, and you get cross-platform support for bin apps. Too much niftiness to ignore, really.

The New Gem Generator is like the rails command for rails applications, but it creates the folders and starting files for a new gem. It’s called newgem.

Tutorial

Aim

To convert the Map by Method (previously called Map by Pluralisation) library into a gem.

See original and demo articles. Sex on a stick – soon to be gemified before your very eyes. (Download instructions for the prebuilt gem)

Installation

> gem install newgem

Download from rubyforge if you have firewall problems (as I do at work) and need to get the gem explicitly first. THEN run the above command in the folder you saved the gem.

Create new gem

> newgem map_by_method
creating: map_by_method
creating: map_by_method/CHANGELOG
creating: map_by_method/README
creating: map_by_method/lib
creating: map_by_method/lib/map_by_method
creating: map_by_method/lib/map_by_method.rb
creating: map_by_method/lib/map_by_method/version.rb
creating: map_by_method/Rakefile
creating: map_by_method/test
creating: map_by_method/test/all_tests.rb
creating: map_by_method/test/test_helper.rb
creating: map_by_method/test/map_by_method_test.rb
creating: map_by_method/examples
creating: map_by_method/bin

Copy in the library

The generated lib/map_by_method.rb file looks like:

Dir['map_by_method/**/*.rb'].sort.each { |lib| require lib }

This will automatically include (require), in alphabetical order, the files in the
lib/map_by_method folder. If you need the files required in a specific order, then do it explicitly here, for example:

require 'foo'
require 'bar'
require 'tar'

or

%w(foo bar tar).each {|lib| require lib}

For this gem there won’t be any additional library files, so we’ll just copy in the following code into the lib/map_by_method.rb file and remove the library loading code.

module MapByMethod
  def self.included(base)
    super

    base.module_eval <<-EOS
      def method_missing(method, *args, &block)
        super
      rescue NoMethodError
        error = $!
        begin
          re = /(map|collect|select|each|reject)_([\\w\\_]+\\??)/
          if (match = method.to_s.match(re))
            iterator, callmethod = match[1..2]
            return self.send(iterator) {|item| item.send callmethod}
          end
          return self.map {|item| item.send method.to_s.singularize.to_sym}
        rescue NoMethodError
          nil
        end
        raise error
      end
    EOS
  end
end

unless String.instance_methods.include? "singularize"
  class String
    def singularize
      self.gsub(/e?s\Z/,'')
    end
  end
end

Array.send :include, MapByMethod

Package your gem into a .gem file

From the root folder of your gem run rake package:

> rake package
(in C:/InstantRails/ruby_apps/map_by_method)
rm -r .config
  Successfully built RubyGem
  Name: map_by_method
  Version: 0.0.1
  File: map_by_method-0.0.1.gem
mv map_by_method-0.0.1.gem pkg/map_by_method-0.0.1.gem

Tada! You are the owner of a gem.

Install your gem onto your machine

Your Ruby (and Rails) applications can only use the gemified libraries once you have installed the packaged gem. This is the same for other people who will use your gem.

Typically you install a gem from a remote gem server such as rubyforge. Today, you will install the gem locally:

> gem install pkg/map_by_method-0.0.1.gem
Attempting local installation of 'pkg/map_by_method-0.0.1.gem'
Successfully installed map_by_method, version 0.0.1
Installing RDoc documentation for map_by_method-0.0.1...

Note that it created and installed RDoc documentation for the library too. Each user automatically has a copy of the generated documentation for your libraries (if you actually added documentation to your code).

Unit testing

Look in the test folder and see that it has created a map_by_method_test.rb unit test file to get you started. Put tests in there. Add more test files. Run rake test from the project’s root folder and watch all your tests succeed or fail. Be good.

Version numbers

Note that the generated gem is map_by_method-0.0.1.gem. The 0.0.1 is the version number of the gem, and you can easily change this as you wish as you gem takes on new features and fixes.

There are two common version number formats:

X.Y.Z – X = major release number, Y = minor release number, Z = patch/bug fix number

or

X.Y.Z.svn = svn is the subversion number at the time the gem was released.

The latter is the default implementation generated by newgem. If you want the simpler version number format (the 1st one), then remove the following line from your Rakefile (around line 13):

REV = File.read(".svn/entries")[/committed-rev="(\d+)"/, 1] rescue nil

Your gem has a prebuilt mechanism for specifying the X.Y.Z portion of the version number.

Go to lib/map_by_method/version.rb

module MapByMethod #:nodoc:
  module VERSION #:nodoc:
    MAJOR = 0
    MINOR = 0
    TINY  = 1

    STRING = [MAJOR, MINOR, TINY].join('.')
  end
end

Change MINOR and TINY to:

    MINOR = 1
    TINY  = 0

And repackage and reinstall your gem:

> rake package
(in C:/InstantRails/ruby_apps/map_by_method)
rm -r .config
  Successfully built RubyGem
  Name: map_by_method
  Version: 0.1.0
  File: map_by_method-0.1.0.gem
mv map_by_method-0.1.0.gem pkg/map_by_method-0.1.0.gem

> gem install pkg/map_by_method-0.1.0.gem
Attempting local installation of 'pkg/map_by_method-0.1.0.gem'
Successfully installed map_by_method, version 0.1.0
Installing RDoc documentation for map_by_method-0.1.0...

And you’re done. Next you would upload your gem to a gem server such as RubyForge, or one your company runs internally to share gems via the “gem install” mechanism.

map_by_method already on RubyForge

If you want the map_by_method gem, its already on RubyForge, so you can remotely install it:

gem install map_by_method

Inspiration

Jay Fields created a great “ruby application setup” script.

[1] That’s not true. I do like you.

I love “map by pluralisation” [now: map_by_method]

Update: this is a gem called map_by_method.

The other day I introduced a new syntax idea that I call “map by pluralisation”. Everyday I use it in code and in the console/irb I fall more in love with its simplicity – both to type and to read.

The following are all equivalent:

>> BankTransaction.columns.names  # map by pluralisation
=> ["id", "amount", "date", "description", "balance"]
>> BankTransaction.columns.name   # singular works too
=> ["id", "amount", "date", "description", "balance"]
>> BankTransaction.columns.map_name  # merge collector (map) + operation (name is method of BankTransaction object)
=> ["id", "amount", "date", "description", "balance"]
>> BankTransaction.columns.collect_name  # merge collector (collect) + operation
=> ["id", "amount", "date", "description", "balance"]

All of which are easier to read and quicker to type than the current equivalents:

>> BankTransaction.columns.map {|p| p.name}  # standard map
=> ["id", "amount", "date", "description", "balance"]
>> BankTransaction.columns.map &:name  # Symbol.to_proc
=> ["id", "amount", "date", "description", "balance"]

You can now type:

>> BankTransaction.columns.select_primary  # merge collector (select) and operator (primary returns true/false on a Column object)
=> [#<...MysqlColumn:0x3c8a4f0 @limit=11, @sql_type="int(11)", @primary=true, @type=:integer, @number=true, @name="id">]

Instead of:

>> BankTransaction.columns.select {|c| c.primary}
>> BankTransaction.columns.select &:primary

Use with ActiveRecords

“Map by pluralisation” is truly wonderful in the console for exploring and collecting data models.

Without knowing a thing about the data model, I bet you can understand either of the following:

@transactions = BankTransaction.find :all, :conditions => ['date = ?', Date.today], :include => [:accounts => [:owner]]
return @transactions.collect_accounts.select_overdrawn?.collect_owner.full_names

Answer: the full name of each owner of an overdrawn bank accounts for all today’s bank transactions.

It reads well as there is a minimum of {, }, |, &, : characters.

Download

Code available here

Tip for use with ActiveRecord Associations

You may need to cast associations to Arrays.

@account.transactions.to_a.map_dates

The result of the transactions assocation on the Account class is not an Array. My initial attempts to provide the “map by pluralisation” code (a method_missing) didn’t work, and casting it to an explicit Array using to_a seemed simple and harmless.

Discussion of Syntax Ideas

Ruby Forum

zip vs transpose

Prelude

If I wanted to create a Hash from two arrays – one containing the keys and the other the values – I have used transpose:

>> keys = %w(name description country)
>> values = ["Dr Nic", "Good lookin'", "Netherlands"]
>> [keys, values].transpose
=> [["name", "Dr Nic"], ["description", "Good lookin'"], ["country", "Netherlands"]]
>> hash = Hash[*[keys, values].transpose.flatten]
=> {"name"=>"Dr Nic", "country"=>"Netherlands", "description"=>"Good lookin'"}

I was happy with that.

The newcomer from outta town…

Today I found zip for Arrays.

>> keys.zip(values)
=> [["name", "Dr Nic"], ["description", "Good lookin'"], ["country", "Netherlands"]]
>> hash = Hash[*keys.zip(values).flatten]
=> {"name"=>"Dr Nic", "country"=>"Netherlands", "description"=>"Good lookin'"}

Now I don’t know which is better. Nor do I know which syntax is cleaner and more meaningful.

Both are as meaningless as each other:

  • How many people remember what transpose means from matrix mathematics at school anyway?
  • How many people would look at keys.zip(values) and guess what the result will be?

Anyone with any thoughts on this?

Turn-based game DSL

Late in the night, whilst the baby feeds, I continue to develop Dr Nic’s Civilizations game. This led me to develop a DSL for the specification of game rules. You need to see this.

Here’s an example definition of some terrain:

  class Desert < Terrain
    title           "Desert"
    letter          "d"
    graphic         "desert"
    movement_cost   1
    defense_bonus   10
    food            0
    shield          1
    trade           0
  end
  class Oasis < Desert
    title           "Oasis"
    special_code    1
    food            3
    shield          1
    trade           0
    graphic         "oasis"
  end

An Oasis is a special version of the Desert terrain, so Ruby subclasses offer a compatible relationship.

This is seriously cool. All in Ruby.

Why write a DSL for something trite like game rules?

The descriptions of terrain (plains, hills, ocean), the improvements (roads, irrigation, mines), etc. all need defining somewhere. Here are the standard options:

  1. Rows in a database, imported into ActiveRecords at startup
  2. Files that are loaded at startup, parsed and converted into an internal data model
  3. Ruby DSL

The sort of configuration we're dealing with is static: its a part of the game design to a certain extent. Yes, I could put the configuration in a database and access/modify it via an admin console, though version control becomes an additional administrative hassle.

Storing game rule configuration in external files makes version control trivial, though there is a one-time cost of implementing special purpose syntax, parsers, internal game data models and testing.

A Ruby DSL is exactly the same as implementing option 2 - the configuration is stored in files, though as the syntax is pure Ruby, there is no need to implement a parser and internal data model.

There was a one time cost for supporting the syntax. But why the lucky stiff paid this cost for me with his traits definition for slaying dragons [1]. So I was free and clear of any actual effort.

"Yes, smart arse, but you still had to write up all those Ruby classes"

An irrelevant argument, you'd have to enter the configuration for any of the 3 options, but...

I wrote a generator to build the Ruby class definitions for me from the Freeciv's GPL rule sets. Sweet.

[1] The definition of the Terrain class, using the traits mechanism, is simple:

  class Terrain
    traits :title,
      :letter,
      :graphic,
      :movement_cost,
      :defense_bonus,
      :food,
      :shield,
      :trade,
      :special_code
  end