CrudVision - Lisa Seelye

July 30, 2008

Fixtures in Rails Suck - this makes them suck less

Filed under: open source, rails, snippet, testing — Lisa Seelye @ 1:31 am

If you've got a boat load of legacy fixtures laying around (and who doesn't?) it can be a pain now that Rails handles the ID of objects so you don't have to.

On the train back from holiday I wrote a snippet of code to migrate them:

RUBY:
  1. def Util.real_yml(klass,col = 'name')
  2.     y = YAML.load_file(File.join(RAILS_ROOT,'test','fixtures',"#{klass.to_s.tableize}.yml"))
  3.     real_names = y.inject([]) { |names,(key,val)| names <<y[key][col] if y[key][col] ;names  }
  4.     real_objs = klass.find :all, :conditions => [ "#{col} IN (?)",real_names ], :order => "#{col} asc"
  5.     yml = ""
  6.     real_objs.each do |real_obj|
  7.       yml += "#{real_obj.name.downcase.gsub(' ','_').gsub('-','')}:\n"
  8.       real_obj.attributes.each do |k,v|
  9.         if k =~ /_id$/ && real_obj.respond_to?(k.gsub(/_id$/,'').to_sym) && ! v.nil?
  10.           n_klass = k.gsub(/_id$/,'').camelize.constantize.find(v) rescue nil
  11.           key = k.gsub(/_id$/,'')
  12.           val = n_klass.name.downcase.gsub(' ','_').gsub('-','')
  13.           yml += "  #{key}: #{val}\n"
  14.         else
  15.           yml += "  #{k}: #{v}\n" unless k.to_s == 'id'
  16.         end
  17.       end
  18.     end
  19.     yml
  20.   end

The use is:

RUBY:
  1. require 'util'
  2. print Util.real_yml(Corporation)

Copy/paste into corporations.yml

So far it's working great!

February 9, 2008

An evil snippet for hash traversal

Filed under: json, rails, ruby, snippet, testing, work — Lisa Seelye @ 12:28 pm

First the problem:

I'm writing a load test framework at work and I need to consume JSON webservice and pass on the output to another request. But I don't always know what the data is that I need to pass on but I do know the basic "path" ("hpath" -- "hash path") to get to the data as the JSON data is uniform.

Now the method:

RUBY:
  1. # Picks out data from a (JSON) decoded hash based on the @passon hash,
  2. # which looks like this:
  3. # { "id" => "packet.products[0].attributes.id",
  4. #   "quantity" => "packet.products[0].attributes.quantity"
  5. # }
  6. # The "id" and "quantity" are the new keys for the return data;
  7. # packet.products[0].attributes.id will look at the value of the id key
  8. # in the attributes hash in the 0th element of the products array in the
  9. # packet hash.
  10. def pick_out_passon(hash_data)
  11.   return {} unless @passon
  12.   returnhash = {}
  13.   nh = hash_data.dup
  14.   @passon.each do |newkey,part_str|
  15.     parts = part_str.split(".").reverse
  16.     while (part = parts.pop) do
  17.       m = part.match(/\[(\d+)\]/)
  18.       index = nil
  19.       if m
  20.         index = m[1].to_i
  21.         part.gsub!(/\[#{m[0]}\]/,'')
  22.       end
  23.       nh = nh.values_at(part).first
  24.       nh = nh.at(index) if index
  25.     end
  26.     returnhash.merge!({ newkey=> nh})
  27.     nh = hash_data.dup
  28.   end
  29.   returnhash
  30. end

What's it do? It does magic!

I'll step through it...

Looks for @passon instance variable and doesn't do anything useful unless it exists.

Duplicate the input hash because the process done is destructive to it and we may need to reuse it.

For each new hash key and "hpath" pair from @passon split up the hpath into its parts and reverse it so Array#pop will work in the while loop to get the next first part to try.

Since each part of the hpath can examine an Array by index it has to be checked for and the index removed from the part (and saved).

Next, investigate the copy of the hash_data, nh by the computed key; if the value in the hash was an Array use the index to get the desired value. Then compute the next part!

Once we're out of parts stuff the new key and data into the returnhash and keep going til there's no more @passon pairs.

And thus some fun code was written.

January 2, 2008

Followup to My First Patch

Filed under: activerecord, open source, oracle, rails, snippet, sql — Lisa Seelye @ 5:50 pm

In reference to the post about My First Patch to the Rails trac I mentioned in the edit of the post that I emailed the maintainer. There's been no response, unfortunately.

So I'll post the patch here so it'll get a bit more visibility and hopefully Oracle users can find it helpful in speeding up rake db:migrate on Oracle systems with a lot of indicies.

Patch to speed up Rails’s OracleAdapter#indexes method

September 23, 2007

Random Snippet: Soundex

Filed under: C, ruby, snippet — Lisa Seelye @ 4:17 pm

This bit of code is the Soundex algorithm based on some C written by a friend of mine (Hi Jen!). She writes really compact C (read: hard to read) and it took me a long time to understand what the code she wrote did.

So here is the annotated version of the code (correlates to the Rule numbers on Wikipedia).

RUBY:
  1. class String
  2.   @@sem = [ 0,1,2,3,0,1,2,0,0,2,2,4,5,5,0,1,2,6,2,3,0,1,0,2,0,2 ] # Letter codes, Rule 3
  3.  
  4.   def to_soundex
  5.     s = self.upcase
  6.     chars = s[1 .. s.size - 1].split(//) # Split the word into characters to encode
  7.     ret = s[0,1] # Rule 1
  8.     chars.each_index do |i|
  9.       c = chars[i]
  10.       next if ( c <'A' or c> 'Z' ) # Not a letter
  11.       next if i>0 and c == chars[i - 1] # Rule 4
  12.       d = @@sem[ c[0] - 'A'[0] ] # Encode it
  13.       ret += d.to_s unless d == 0 # Rule 2
  14.     end
  15.     ret += "0000" # Rule 5
  16.     ret[0..3] # Rule 5
  17.   end
  18. end

It was fun to write and it works well.

August 28, 2007

Major Reve Changes

Filed under: api, eve online, evedb.info, reve, ruby, snippet — Lisa Seelye @ 10:52 pm

Greetings dear readers.

The CCP guy (Garthagk) behind the Eve Online API we all use really has a hard task. As the API becomes more and more popular the stress on the webservers hosting the API and the database servers increases. Bossmen and bosswomen may cut the API from us to save resources (that would suck). It's up to us, the community, to do what we can to lessen the load on CCP's servers.

Garthagk provided us with a cacheduntil (Reve::API.cached_until in Reve). Up unil now Reve didn't privde any mechanism for really using this. Sure, you could use just the cached_until if it's just one character. But one of my hopes is that Reve will be used by operators of multi-user sites (like my vapourware evedb.info) that will aggregate many users' data. Cached_until isn't enough.

Enter the protected method Reve::API#compute_hash. (As of revision 22.)

After each API call Reve will compute a hash to represent that request. This will be available through the last_hash. This will tell you what the hash is after the call. But to respect the cached_until it's polite to check the current time against the "Last Call's" cached_until value. The last call will have a hash and that hash will be the key to the cached_until. Each API call now takes a hash for its parameters intead of the old style comma style. Treat the hash as a of transaction ID.

For a simple example let's get the alliances list:

RUBY:
  1. require 'reve'
  2. api = Reve::API.new
  3. alliances = api.alliances
  4. p api.last_hash # =&gt; 'eve/Alliances.xml.aspx'
  5. api.cached_until

For this simple call nothing has changed except the Reve library presents more info for the user. Store cached_until and last_hash in a database.

For the new parameter to get the hash:

RUBY:
  1. require 'reve'
  2. api = Reve::API.new
  3. a_hash = api.alliances :just_hash =&gt; true
  4. p a_hash # =&gt; 'eve/Alliances.xml.aspx'

A complex example:

RUBY:
  1. require 'reve'
  2. api = Reve::API.new('userid','apikey')
  3. sheet = api.character_sheet :characterid =&gt; 0123456789

Get the hash:

RUBY:
  1. require 'reve'
  2. api = Reve::API.new('userid','apikey')
  3. h_sheet = api.character_sheet :characterid =&gt; 0123456789, :just_hash =&gt; true

With luck this post is clear for Reve's users and all you guys will be kind to the API so we have it in the future.

August 15, 2007

Rails Polymorphic Associations

Filed under: evedb.info, rails, snippet — Lisa Seelye @ 10:32 pm

This is good timing. I need to know about this stuff tomorrow to help a colleague out. Rails Polymorphic Associations were a bit mysterious to me and so I didn't use them when I first started with Rails but the time has come to put them to work.

First, the scenario:

A Player and a Corporation both may have many Wallet Balance Entries and a Wallet Balance Entry belongs to either a Player or a Corporation.

The Models are this:

RUBY:
  1. # app/models/wallet_balance_entry.rb
  2. class WalletBalanceEntry &lt;ActiveRecord::Base
  3. belongs_to :wallet_balance_owner, :polymorphic =&gt; true
  4. end

And (just the Player model)

RUBY:
  1. # app/models/player.rb
  2. class Player &lt;ActiveRecord::Base
  3. has_many :wallet_balance_entries, :as =&gt; :wallet_balance_owner
  4. end

The relevant bits of the WalletBalanceEntry schema look like this:

RUBY:
  1. create_table "wallet_balance_entries", :force =&gt; true do |t|
  2. t.string   "wallet_balance_owner_type"
  3. t.integer  "wallet_balance_owner_id"

Note that wallet_balance_owner_type and wallet_balance_owner_id match the name that the Player model specifies with :as => :wallet_balance_owner. The Corporation model will have the exact same line and it will work! Should Alliances come to have many wallet balance entries the Alliance model will have this same line as well.

August 6, 2007

Postgres “gotcha”

Filed under: postgres, snippet — Lisa Seelye @ 8:45 am

In my evedb.info efforts I need a quick way to expire old market orders. The CSV has a duration column that is an integer number of days. Postgres allows its users to typecast between various data types in SQL so there doesn't need to be a SELECT duration FROM ... ; (in code) to_delete = Time.now + duration.days ; DELETE FROM ... WHERE duration < to_delete;

It just so happens that the form for casting an integer to do math with a timestamp is a bit funky. An integer may not be directly added to a timestamp. The magic is:

SQL:
  1. DELETE
  2. FROM foo
  3. WHERE now()> (created_on + CAST( (duration || ' days') AS INTERVAL));

The || ' days' I don't understand just yet but it works!

August 5, 2007

Ruby Inject Love

Filed under: evedb.info, ruby, snippet — Lisa Seelye @ 12:06 pm

[ Quick edit: Yeah, I screwed up the title and that botched the permalink and now I can't change it. ]

On evedb.info I have Market categories set up as a tree (acts_as_tree) which means when drilling down the categories: "Ammunition & Charges -> Frequency Crystals -> Advanced Beam Laser Crystals", for example I would like to create breadcrumbs to get back to those parent categories.

Before I got Enumerable#inject working I was mangling with all sorts of recursion. It wasn't pretty.

Then along came the day when I wanted to rewrite it to make sense!

RUBY:
  1. def marketHierarchy(cat)
  2. cat.ancestors.reverse.inject("") { |link_str,a_cat| link_str += link_to(h(a_cat),market_path(a_cat)) + " -&gt; " }
  3. end

This bit of voodoo works brilliantly.
What it does, for those who aren't familiar with Enumerable#inject is:

For each element in the ancestors of the passed MarketCategory do:

  1. Do the block, but first initialize link_str to ""
  2. Append a link to link_str and ->
  3. After the link is appended pass set link_str for the next element in the ancestors list to the result of the block (link_str)

And then just return the result of the block, the final link_str.

The Enumerable#inject method is really cool!

July 25, 2007

What’s better than CRUD?

Filed under: api, evedb.info, rails, snippet — Lisa Seelye @ 7:50 am

DRY CRUD!

That is, of course, "Don't repeat yourself".

With my evedb.info site I have to essentially rsync two databases whilst mangling schema. The process will take FOREVER (Mainly because I'm not multithreading this process) but it's automated.

I kept having to write CRUD methods in all of my controllers to deal with updating evedb.info as I talked about in this linked post and they were ALL the same. So being the lazy programmer I am I put them all in the ApplicationController:

Here's the update method

RUBY:
  1. def update
  2.     singular = params[:controller].singularize
  3.     camel = singular.camelize
  4.     klass = camel.constantize
  5.     @temp = klass.new params[singular]
  6.     @real = klass.find params[:id]
  7.     changes = object_diff(@real,@temp)
  8.     logger.info "Changes: #{changes.inspect}"
  9.     if changes.keys.size == 0
  10.       render :nothing => true, :status => :ok
  11.       return
  12.     end
  13.     if @real.update_attributes(changes)
  14.       render :nothing => true, :status => :ok
  15.     else
  16.       render :json => @real.errors.to_json, :status => 400
  17.     end
  18.   end

By abusing Rails's constantize I can mangle the controller's name ("corporations") into the model it's handling: Corporation and treat it klass as if it was actually spelled Corporation. Personally I think that is very cool.

object_diff is a method I wrote to find the differences between two ActiveRecord::Base derrived objects. Loop through attributes hash and note things that change. changes is those attributes that have changed.

The only part I haven't been able to DRY out is the index method which always returns YAML for my scripts but only the IDs of the objects.

RUBY:
  1. if params[:format] != 'yaml'
  2.   @corporations = Corporation.find normal finder
  3. end
  4. respond_to do |format|
  5.   format.html
  6.   format.yaml { render :text => Corporation.find(:all, :select => 'id', :order => 'id').to_yaml }
  7. end

But I don't think I can DRY it out becase Corporation YAML finder actually selects another column that my script uses. Oh well.

July 6, 2007

Rails JSON is broken

Filed under: json, rails, ruby, snippet, work — Lisa Seelye @ 11:12 pm

At work yesterday I found out that by default ActiveSupport::JSON.unquote_hash_key_identifiers is true. This means that a JSON string looks like:

{attributes: {name: "0-3VW8 I", coordinate_id: "6229", system_id: "30000995", id: "40063299", igbtype: "7"}}

Flash JSON decoders (and probably others) expect the above attributes word to be doublequoted. This is achieved (after much Googling) by setting ActiveSupport::JSON.unquote_hash_key_identifiers=false.

But it doesn't stop there. In the above JSON output the IDs are quoted, that means they're encoded as strings and not integers. This is against the spec.

This means that

RUBY:
  1. ActiveSupport::JSON.unquote_hash_key_identifiers = false
  2. planet = Planet.find :first
  3. print planet.to_json # =&gt; {"attributes": {name: "0-3VW8 I", coordinate_id: "6229", system_id: "30000995", id: "40063299", igbtype: "7"}}

The integers should not be double quoted.

With the help of lifo from #rails-contrib the problem seems to have been narrowed down to instance_values/instance_variable_get borking things up. From my poking around (n.b., I'm not all that experienced with the source for Rails itself! I may be wrong) the issue seems to lay within vendor/rails/activesupport/lib/active_support/json/encoders/object.rb. This bit of code seems to work for AR::Base derived models:

RUBY:
  1. class ActiveRecord::Base
  2. def to_json
  3. ret = ActiveSupport::JSON.unquote_hash_key_identifiers ? "{attributes: " : '{"attributes": '
  4. ret + self.attributes.to_json + '}'
  5. end
  6. end

Edit (10 July 2007): It seems that this isn't exactly a drop-in replacement fix: If a model has associations loaded (Foo.find :first, :include => :bars) the above JSON-fixing snippet will not work because it looks at attributes only and not the loaded associations. I'll have to work around that.

Older Posts »

Powered by WordPress