kid_drew
kid_drew

Reputation: 3995

Ruby class with array attribute

I have a non-database-backed class in Ruby:

class User
  attr_accessor :countries
end

I want countries to simply be an array of ISO country codes (US, GB, CA, AU, etc) and I don't want to build a separate model to hold each. Is there a magic way to make Ruby understand that :countries is an array and treat it accordingly, or do I need to write the countries and countries= methods?

I tried just setting the countries array with user.countries = ['US'], and I'm getting a NoMethodError.

Upvotes: 2

Views: 4188

Answers (2)

Dave Newton
Dave Newton

Reputation: 160191

The type of a variable doesn't matter in Ruby.

attr_accessor just creates getter and setter methods that set and return instance variables; @countries in this case. You can set the instance variable to your array, or use the setter:

class User
  attr_accessor :countries

  def initialize
    @countries = %w[Foo Bar Baz]
    # Or...
    self.countries = %w[Foo Bar Baz]
  end
end

> puts User.new.countries
=> ["Foo", "Bar", "Baz"]

Personally I prefer using the instance variable instead of self.xxx; it's too easy to forget the self. bit and you end up setting a local variable, leaving the instance variable nil. I also think it's ugly.

If the countries won't be changing between instances, why not a constant?

Edit/Clarification

Tadman's point is well-taken, e.g., this diatribe on state. The circumtances under which I don't care about that are limited to small, self-controlled, stand-alone classes. There are inherent risks in making those assumptions, the level of those risks is project-dependent.

Upvotes: 6

Stefan
Stefan

Reputation: 114158

Looks like countries should be a constant:

class User
  COUNTRIES = %w(
    AF AX AL DZ AS AD AO AI AQ AG AR AM AW AU AT AZ BS BH BD BB BY BE BZ BJ BM
    BT BO BQ BA BW BV BR IO BN BG BF BI KH CM CA CV KY CF TD CL CN CX CC CO KM
    CG CD CK CR CI HR CU CW CY CZ DK DJ DM DO EC EG SV GQ ER EE ET FK FO FJ FI
    FR GF PF TF GA GM GE DE GH GI GR GL GD GP GU GT GG GN GW GY HT HM VA HN HK
    HU IS IN ID IR IQ IE IM IL IT JM JP JE JO KZ KE KI KP KR KW KG LA LV LB LS
    LR LY LI LT LU MO MK MG MW MY MV ML MT MH MQ MR MU YT MX FM MD MC MN ME MS
    MA MZ MM NA NR NP NL NC NZ NI NE NG NU NF MP NO OM PK PW PS PA PG PY PE PH
    PN PL PT PR QA RE RO RU RW BL SH KN LC MF PM VC WS SM ST SA SN RS SC SL SG
    SX SK SI SB SO ZA GS SS ES LK SD SR SJ SZ SE CH SY TW TJ TZ TH TL TG TK TO
    TT TN TR TM TC TV UG UA AE GB US UM UY UZ VU VE VN VG VI WF EH YE ZM ZW
  ).freeze
end

User::COUNTRIES.include? "US" #=> true

freeze prevents modifications:

User::COUNTRIES.delete "US"   #=> RuntimeError: can't modify frozen Array

Update

The problem here is that your countries array has to be persisted somehow. You are mentioning has_many so Rails seems to be involved. You can use ActiveRecord's serialize method:

class User < ActiveRecord::Base
  serialize :countries
end

This will save the countries attribute to the database as an object and retrieve it as such:

u = User.new
u.countries = ["US", "CA"]
u.save

u = User.last
u.countries
#=> ["US", "CA"]

It's converted to and from YAML internally, so the users table looks like:

mysql> SELECT * FROM users;
+----+-------------------+---------------------+---------------------+
| id | countries         | created_at          | updated_at          |
+----+-------------------+---------------------+---------------------+
|  1 | ---\n- US\n- CA\n | 2013-09-24 18:24:03 | 2013-09-24 18:24:03 |
+----+-------------------+---------------------+---------------------+
1 row in set (0,00 sec)

Upvotes: 4

Related Questions