Very simple example:
Model:
require 'inventory'
class CustomerOrder < ActiveRecord::Base
validates_presence_of :name
validate :must_have_at_least_one_item, :items_must_exist
before_save :convert_to_internal_items
attr_accessor :items
after_initialize do
#convert the internal_items string into an array
if internal_items
self.items ||= self.internal_items.split(',').collect { |x| x.to_i }
else
# only clobber it if it hasn't been set yet, like in the constructor
self.items ||= []
end
end
private
def convert_to_internal_items
#TODO: convert the items array into a string
self.internal_items = self.items.join(',')
end
def must_have_at_least_one_item
self.items.size >= 1
end
def items_must_exist
self.items.all? do |item|
Inventory.item_exists?(item)
end
end
end
Inventory is a singleton that should provide access to another service out there.
class Inventory
def self.item_exists?(item_id)
# TODO: pretend real code exists here
# MORE CLARITY: this code should be replaced by the mock, because the actual
# inventory service cannot be reached during testing.
end
end
Right now the service does not exist, and so I need to mock out this method for my tests. I'm having trouble doing this the Right Way(tm). I'd like to have it be configurable somehow, so that I can put in the mock during my tests, but have the normal code run in the real world.
There's probably something I'm not wrapping my head around correctly.
EDIT: to be more clear: I need to mock the Inventory class within the validation method of the model. Eventually that will talk to a service that doesn't exist right now. So for my tests, I need to mock it up as if the service I were talking to really existed. Sorry for the confusion :(
Here's what I'd like to have in the specs:
describe CustomerOrder do
it "should not accept valid inventory items" do
#magical mocking that makes Inventory.item_exists? return what I want
magic.should_receive(:item_exists?).with(1).and_return(false)
magic.should_receive(:item_exists?).with(2).and_return(true)
co = CustomerOrder.new(:name => "foobar", :items => [1,2]
co.should_not be_valid
end
it "should be valid with valid inventory items" do
#magical mocking that makes Inventory.item_exists? return what I want
magic.should_receive(:item_exists?).with(3).and_return(true)
magic.should_receive(:item_exists?).with(4).and_return(true)
co = CustomerOrder.new(:name => "foobar", :items => [3,4]
co.should be_valid
end
end
Using rails 3.0.3, rspec 2 and cucumber. Of course, only the rspec part matters.
require 'spec_helper'
describe CustomerOrder do
it "is invalid without an existing Inventory item" do
item = mock('item')
customer = Customer.new(:name=>"Moe")
customer.stub(:items) { [item] }
Inventory.should_receive(:item_exists?).with(item).and_return(true)
customer.should_not be_valid
end
end
Note: untested.
The way I ended up solving this follows
Inventory class:
require 'singleton'
class Inventory
include Singleton
def self.set_mock(mock)
#mock = mock
end
def self.item_exists?(item_id)
return #mock.item_exists?(item_id) if #mock
# TODO: how should I stub this out for the api
end
end
CustomerOrder model:
require 'inventory'
class CustomerOrder < ActiveRecord::Base
validates_presence_of :name
validate :must_have_at_least_one_item, :items_must_exist
before_save :convert_to_internal_items
attr_accessor :items
after_initialize do
#convert the internal_items string into an array
if internal_items
self.items ||= self.internal_items.split(',').collect { |x| x.to_i }
else
# only clobber it if it hasn't been set yet, like in the constructor
self.items ||= []
end
end
private
def convert_to_internal_items
#TODO: convert the items array into a string
self.internal_items = self.items.join(',')
end
def must_have_at_least_one_item
errors.add(:items, "Must have at least one item") unless self.items.size >= 1
end
def items_must_exist
failed = self.items.find_all do |item|
!Inventory.item_exists?(item)
end
if !failed.empty? then
errors.add(:items, "Items with IDs: [#{failed.join(' ')}] are not valid")
end
end
end
CustomerOrder specs:
require 'spec_helper'
describe CustomerOrder do
fixtures :all
before do
fake = double('fake_inventory')
fake.stub(:item_exists?) do |val|
case val
when 1
true
when 2
true
when 3
false
end
end
Inventory.set_mock(fake)
#GRR, skipping my fixtures right now
#valid_order = CustomerOrder.new(:name => "valid order",
:items => [1,2])
end
it "should require a name and at least one item" do
co = CustomerOrder.new(:name => "valid", :items => [1])
co.should be_valid
end
it "should not be valid without any items" do
#valid_order.items = []
#valid_order.should_not be_valid
end
it "should not be valid without a name" do
#valid_order.name = nil
#valid_order.should_not be_valid
end
it "should expose items instead of internal_items" do
#valid_order.should respond_to(:items)
end
it "should be able to treat items like an array" do
#valid_order.items.size.should == 2
#valid_order.items.should respond_to(:<<)
#valid_order.items.should respond_to(:[])
end
it "should store items internally as a comma separated string" do
co = CustomerOrder.new(:name => "name", :items => [1,2])
co.save!
co.internal_items.should == "1,2"
end
it "should convert items to internal_items for saving" do
co = CustomerOrder.new(:name => "my order",
:items => [1,2])
co.name.should == "my order"
co.save!
co.internal_items.should == "1,2"
end
it "loads items from the database into the items array correctly" do
co = CustomerOrder.new(:name => "woot", :items => [2,1])
co.save.should == true
co2 = CustomerOrder.find_by_name("woot")
co2.items.should == [2,1]
end
it "is not valid with items that don't exist" do
#valid_order.items = [3,2,1]
#valid_order.should_not be_valid
end
it "ensures that items exist to be valid" do
#valid_order.items = [1,2]
#valid_order.should be_valid
end
end
This solution works, although it's probably not the best way to inject a mock into the Inventory Service at runtime. I'll try to do a better job of being more clear in the future.
Related
I have run into confusion tying together a test double and stubbing it. My question is - what is the most appropriate way to test the confirm_purchase_order and create_order methods in class PurchaseOrder?
I have included the relevant code following code:
class PurchaseOrder
attr_reader :customer, :products
def initialize(customer)
#products = {}
#customer = customer
end
....some other methods
def add_product(product, quantity = 1)
#products[product] = (#products[product] ? #products[product] + quantity : quantity )
puts "You haved added #{quantity} #{product.title}'s to your purchase order"
end
def confirm_purchase_order
purchase_order_total
raise "Your PO appears to be empty! Add some products and try again." unless self.total.to_f.round(2) > 0
create_order
create_invoice
return "We have generated an Invoice and created an order."
end
def create_order
order = Order.new(customer)
order.products = #products.clone
end
def create_invoice
invoice = Invoice.new(customer)
invoice.products = #products.clone
end
end
class Order
attr_reader :customer
attr_accessor :status, :total, :products
def initialize(customer)
#products = {}
#status = :pending
#customer = customer
end
class Customer
attr_reader :name, :type
def initialize(name, type)
#name = name.to_s
#type = type.to_sym
end
class Invoice
attr_reader :customer, :products
attr_accessor :total
def initialize(customer, products)
#products = {}
#customer = customer
#payment_recieved = false
end
end
I want to test the confirm_purchase_order method as well as the create_order method in class PurchaseOrder. My approach so far:
I need some object doubles and an actual PurchaseOrder object
describe PurchaseOrder do
let(:product) { double :product, title: "guitar", price: 5 }
let(:order) { instance_double(Order) }
let(:customer) { double :customer, name: "Bob", type: :company }
let(:products) { {:product => 1} }
let(:purchase_order) { PurchaseOrder.new(customer) }
describe "#create_order" do
it "returns an order" do
expect(Order).to receive(:new).with(customer).and_return(order)
allow(order).to receive(products).and_return(???products??!)
purchase_order.add_product(product, 1)
purchase_order.create_order
expect(order.products).to eq (products)
end
end
end
I have also looked at the use of:
# order.stub(:products).and_return(products_hash)
# allow_any_instance_of(Order).to receive(:products) { products_hash }
# order.should_receive(:products).and_return(products_hash)
To setup the order double to return a products hash when order.products is called, but these all feel like they are 'rigging' the test too much. What is the most appropriate way to test the confirm_purchase_order and create_order methods in class PurchaseOrder?
It seems to me that perhaps you're giving PurchaseOrder too much responsibility. It now has intimate knowledge about Order and Invoice.
I'd perhaps test the current implementation like this:
it "returns an order with the same products" do
expect_any_instance_of(Order).to receive(:products=).with(products: 1)
purchase_order.add_product(product, 1)
expect(purchase_order.create_order).to be_a(Order)
end
But maybe it could make sense to decouple PurchaseOrder from Order and Invoice a little bit and do something like this:
class Invoice
def self.from_purchase_order(purchase_order)
new(purchase_order.customer, purchase_order.products.clone)
end
end
class Order
def self.from_purchase_order(purchase_order)
new.tap(purchase_order.customer) do |invoice|
invoice.products = purchase_order.products.clone
end
end
end
class PurchaseOrder
# ...
def create_order
Order.from_purchase_order(self)
end
def create_invoice
Invoice.from_purchase_order(self)
end
end
describe PurchaseOrder do
let(:customer) { double('a customer')}
let(:purchase_order) { PurchaseOrder.new(customer) }
describe "#create_order" do
expect(Order).to receive(:from_purchase_order).with(purchase_order)
purchase_order.create_order
end
describe "#create_invoice" do
expect(Order).to receive(:from_purchase_order).with(purchase_order)
purchase_order.create_order
end
end
describe Order do
describe '.from_purchase_order' do
# test this
end
end
describe Order do
describe '.from_purchase_order' do
# test this
end
end
This way you let the Order and Invoice classes know how to build themselves from a PurchaseOrder. You can test these class methods separately. The tests for create_order and create_invoice become simpler.
Some other things I thought of:
For products, try using a Hash with a default proc:
#products = Hash.new { |hash, unknown_key| hash[unknown_key] = 0 }
This way, you can always safely do #products[product] += 1.
I am confused why my simple ruby object is not converting to json.
>irb
>
require 'json'
class User
attr_accessor :name, :age
def initialize(name, age)
#name = name
#age = age
end
end
u1 = User.new("a", 1)
u2 = User.new("b", 2)
puts u1.to_json
"\"#<User:0x000001010e9f78>\""
What am I missing?
I want to then store these objects into an array collection, and then convert the entire collection to json.
users = []
users << User.new("a", 1)
users << User.new("b", 2)
users.to_json
Note: This is not using Rails, just plain old Ruby!
I want my json to be an array of user objects.
[
{"name": "john", "age": 22},
{"name": "john1", "age": 23}
{"name": "john2", "age": 24}
]
The default implementation of to_json is quite simple and clearly is not doing what you would expect. And this is expected: you need to write code to explain to the interpreter how you want your program to behave.
It's a common standard to provide both a to_json and as_json method. The first latter returns a JSON-serializable version of the instance (generally a Hash), the latter is the actual JSON output.
class User
attr_accessor :name, :age
def initialize(name, age)
#name = name
#age = age
end
def as_json(*)
{ name: #name, age: #age }
end
def to_json(*)
as_json.to_json()
end
end
Here's the output
2.3.0 :034 > u1.as_json
=> {:name=>"a", :age=>1}
2.3.0 :035 > puts u1.to_json
{"name":"a","age":1}
=> nil
With a little effort you can change the as_json to automatically collect all the instance variables. However, I discourage this approach as you may end-up serializing sensitive attributes you don't really want to share (like passwords).
By default, classes cannot be made into JSON strings. You must have a to_json method in your class, so you can make it inherit from this class (type class User < JSONable):
class JSONable
def to_json
hash = {}
self.instance_variables.each do |x|
hash[x] = self.instance_variable_get x
end
return hash.to_json
end
end
Then, you can call to_json and it will work properly.
Test:
$ irb
irb(main):001:0> require 'json'
=> true
irb(main):002:0> class JSONable
irb(main):003:1> def to_json
irb(main):004:2> hash = {}
irb(main):005:2> self.instance_variables.each do |x|
irb(main):006:3* hash[x] = self.instance_variable_get x
irb(main):007:3> end
irb(main):008:2> return hash.to_json
irb(main):009:2> end
irb(main):010:1> end
=> nil
irb(main):011:0> class User < JSONable
irb(main):012:1> attr_accessor :name, :age
irb(main):013:1>
irb(main):014:1* def initialize(name, age)
irb(main):015:2> #name = name
irb(main):016:2> #age = age
irb(main):017:2> end
irb(main):018:1> end
=> nil
irb(main):019:0>
irb(main):020:0* user = User.new("hi",3)
=> #<User:0x007fd6c8af0a90 #name="hi", #age=3>
irb(main):021:0> puts user.to_json
{"#name":"hi","#age":3}
I've been tasked with writing a function to validate that the first name and surname are not the same in the code below. I have to use ActiveModel::Validation and ActiveModel::Errors and if the two names are the same it should give an error message "Nope".
I have very little ruby experience, but here's my attempt:
require 'active_model'
class Person
include ActiveModel::Validations
validate :test
validates_presence_of :first
validates_presence_of :last
def initialize(first, last)
#first = first
#last = last
end
def test
errors.add_to_base("Nope") if #first == #last
end
end
a = Person.new("James", "James")
b = Person.new("James")
So I get an error message when I try to instantiate b but that's just a Ruby error because my function is missing arguments. I'm sure this is probably pretty simple, but I'd be really grateful for any help.
Check https://github.com/rails/rails/tree/master/activemodel for more info:
require 'active_model'
class TestValidator < ActiveModel::Validator
def validate(record)
record.errors[:base] = "First and last can't be the same" if record.first.presence == record.last.presence
end
end
class Person
include ActiveModel::Model
attr_accessor :first, :last
validates_presence_of :first, :last
validates_with TestValidator
end
p = Person.new(first: "Nick", last: "Nick")
puts p.valid? # => false
puts p.errors.full_messages # => First and last can't be the same
p = Person.new(first: "Nick", last: "Doe")
puts p.valid? # => true
I came up with my own solution to this, what I was looking for was:
require 'active_model'
class Person
include ActiveModel::Validations
attr_accessor :first, :last
validate :first_does_not_equal_last
def first_does_not_equal_last
if #first == #last
self.errors[:base] << "First cannot be the same as Last"
end
end
def initialize(first, last)
#first = first
#last = last
end
end
For instance,
s1 = Student.new(1, "Bob", "Podunk High")
hash[1] = s1
puts hash[1].name #produces "Bob"
s1.id = 15
puts hash[15].name #produces "Bob"
puts hash[1].name #fails
This is not exactly Hash-like behavior and insertions with the wrong key still needs to be defined.
While I can certainly roll my own container that behaves this way but it will be hard to make it fast, ie not search through the whole container every time [] is called. Just wondering if someone smarter has already made something I can steal.
EDIT: Some good ideas below helped me focus my requirements:
avoid the O(n) lookup time
allow multiple containers to the same object (association not composition)
have different data types (eg. that might use name instead of id) without too much reimplementation
You can implement it yourself.
Look at the draft solution:
class Campus
attr_reader :students
def initialize
#students = []
end
def [](ind)
students.detect{|s| s.id == ind}
end
def <<(st)
raise "Yarrr, not a student" if st.class != Student
raise "We already have got one with id #{st.id}" if self[st.id]
students << st
end
end
class Student
attr_accessor :id, :name, :prop
def initialize(id, name, prop)
#id, #name, #prop = id, name, prop
end
end
campus = Campus.new
st1 = Student.new(1, "Pedro", "Math")
st2 = Student.new(2, "Maria", "Opera")
campus << st1
campus << st2
campus[1]
#=> Student...id:1,name:pedro...
campus[2].name
#=> Maria
campus[2].id = 10
campus[2]
#=> error
campus[10].name
#=> Maria
Or you can play around Array class (or Hash, if you really need it):
class StrangeArray < Array
def [](ind)
self.detect{|v| v.id == ind} || raise "nothing found" # if you really need to raise an error
end
def <<(st)
raise "Looks like a duplicate" if self[st.id]
self.push(st)
end
end
campus = StrangeArray.new
campus << Student.new(15, 'Michael', 'Music')
campus << Student.new(40, 'Lisa', 'Medicine')
campus[1]
#=> error 'not found'
campus[15].prop
#=> Music
campus[15].id = 20
campus[20].prop
#=> Music
etc
And after #tadman's correct comment you can use reference to your hash right into your Student class:
class Student
attr_accessor :name, :prop
attr_reader :id, :campus
def initialize(id, name, prop, camp=nil)
#id, #name, #prop = id, name, prop
self.campus = camp if camp
end
def id=(new_id)
if campus
rase "this id is already taken in campus" if campus[new_id]
campus.delete id
campus[new_id] = self
end
#id = new_id
end
def campus=(camp)
rase "this id is already taken in campus" if camp[id]
#campus = camp
camp[#id] = self
end
end
campus = {}
st1 = Student.new(1, "John", "Math")
st2 = Student.new(2, "Lisa", "Math", campus)
# so now in campus is only Lisa
st1.campus = campus
# we've just pushed John in campus
campus[1].name
#=> John
campus[1].id = 10
campus[10].name
#=> John
While the Hash object might not behave the way you want it to, you can always customize the objects being inserted to be hashed a particular way.
You can do this by adding two new methods to your existing class:
class Student
def hash
self.id
end
def eql?(student)
self.id == student.id
end
end
By defining hash to return a value based on id, Hash will consider these two candidates for the same spot in the hash. The second definition declares "hash equivalence" between any two objects that have the same hash value.
This will work well provided your id values fit into a conventional 32-bit Fixnum and aren't 64-bit BIGINT database values.
As fl00r points out, this will only work if your id is immutable. For most databases this tends to be the case. Changing id on the fly is probably a really bad idea, though, as it can lead to total chaos and mind-blowing bugs.
This is a hard problem. Database vendors can make money because it is a hard problem. You are basically looking to implement traditional RDBMS indices: search through derived data, to provide fast lookup to the data it was derived from, while allowing that data to change. If you want to access the data from multiple threads, you'll quickly run into all the issues that make it hard to make a database ACID compliant.
I suggest putting the data into a database, adding the necessary indices and letting the database -- an application optimized for this exact purpose -- do the work.
The container must be notified when your key has been changed, otherwise you must search the key on the fly in lg(n).
If you rarely change key and lookup a lot, just rebuild the hash:
def build_hash_on_attribute(objects, attribute)
Hash[objects.collect { |e| [e.send(method), e] }]
end
s1 = OpenStruct.new id: 1, name: 's1'
h = build_hash_on_attribute([s1], :id)
h[1].name # => 's1'
h[1].id = 15
# rebuild the whole index after any key attribute has been changed
h = build_hash_on_attribute(h.values, :id)
h[1] # => nil
h[15].name # => 's1'
Update 02/12: Add a solution using observer pattern
Or you do need such automatically index building, you can use observer pattern like below or decorator pattern. But you need to use the wrapped objects in decorator pattern.
gist: https://gist.github.com/1807324
module AttrChangeEmitter
def self.included(base)
base.extend ClassMethods
base.send :include, InstanceMethods
end
module ClassMethods
def attr_change_emitter(*attrs)
attrs.each do |attr|
class_eval do
alias_method "#{attr}_without_emitter=", "#{attr}="
define_method "#{attr}_with_emitter=" do |v|
previous_value = send("#{attr}")
send "#{attr}_without_emitter=", v
attr_change_listeners_on(attr).each do |listener|
listener.call self, previous_value, v
end
end
alias_method "#{attr}=", "#{attr}_with_emitter="
end
end
end
end
module InstanceMethods
def attr_change_listeners_on(attr)
#attr_change_listeners_on ||= {}
#attr_change_listeners_on[attr.to_sym] ||= []
end
def add_attr_change_listener_on(attr, block)
listeners = attr_change_listeners_on(attr)
listeners << block unless listeners.include?(block)
end
def remove_attr_change_listener_on(attr, block)
attr_change_listeners_on(attr).delete block
end
end
end
class AttrChangeAwareHash
include Enumerable
def initialize(attr = :id)
#attr = attr.to_sym
#hash = {}
end
def each(&block)
#hash.values.each(&block)
end
def on_entity_attr_change(e, previous_value, new_value)
if #hash[previous_value].equal? e
#hash.delete(previous_value)
# remove the original one in slot new_value
delete_by_key(new_value)
#hash[new_value] = e
end
end
def add(v)
delete(v)
v.add_attr_change_listener_on(#attr, self.method(:on_entity_attr_change))
k = v.send(#attr)
#hash[k] = v
end
alias_method :<<, :add
def delete(v)
k = v.send(#attr)
delete_by_key(k) if #hash[k].equal?(v)
end
def delete_by_key(k)
v = #hash.delete(k)
v.remove_attr_change_listener_on(#attr, self.method(:on_entity_attr_change)) if v
v
end
def [](k)
#hash[k]
end
end
class Student
include AttrChangeEmitter
attr_accessor :id, :name
attr_change_emitter :id, :name
def initialize(id, name)
self.id = id
self.name = name
end
end
indexByIDA = AttrChangeAwareHash.new(:id)
indexByIDB = AttrChangeAwareHash.new(:id)
indexByName = AttrChangeAwareHash.new(:name)
s1 = Student.new(1, 'John')
s2 = Student.new(2, 'Bill')
s3 = Student.new(3, 'Kate')
indexByIDA << s1
indexByIDA << s3
indexByIDB << s1
indexByIDB << s2
indexByName << s1
indexByName << s2
indexByName << s3
puts indexByIDA[1].name # => John
puts indexByIDB[2].name # => Bill
puts indexByName['John'].id # => 1
s2.id = 15
s2.name = 'Batman'
p indexByIDB[2] # => nil
puts indexByIDB[15].name # => Batman
indexByName.each do |v|
v.name = v.name.downcase
end
p indexByName['John'] # => nil
puts indexByName['john'].id # => 1
p indexByName.collect { |v| [v.id, v.name] }
# => [[1, "john"], [3, "kate"], [15, "batman"]]
indexByName.delete_by_key 'john'
indexByName.delete(s2)
s2.id = 1 # set batman id to 1 to overwrite john
p indexByIDB.collect { |v| [v.id, v.name] }
# => [[1, "batman"]]
p indexByName.collect { |v| [v.id, v.name] }
# => [[3, "kate"]]
#!/usr/bin/env ruby
# this is the data I have
#data = {
:student => {
:id => '123477',
:first_name => 'Lazlo',
:last_name =>'Fortunatus',
:email=>'Lazlo#fortunatus.org'
},
:contact_info => {
:telephone=>'1 415 222-2222',
:address => '123 Main St',
:city =>'Beverly Hills',
:state=>'California',
:zip_code=>90210,
:social_security_number =>'111-11-1111'
}
}
class Student
# not fully implemented - this is what I need help on.
def get_id_original
# I need this to return the value #data[:student][:id]
end
def get_city_original
# I need this to return the value #data[:contact_info][:city]
end
end
s = Student.new
# this is the original method
# how can I access the #data variable here I tried #data[:student][:id] doesnt work
# I realize that data is outside of the scope of this method. However, is there any way!
s.get_id_original
# My goal is to have a singleton method that acts exactly like get_id_original,
# but get_id_original doesn't work.
def s.id
get_id_original
end
It can be done!
It didn't at first work because #data is an instance attribute of the top level object, so even though Student is derived from Object the attribute isn't in the new instance.
But you can pass self into s.id, and so then the only thing you need to add is an accessor for the data attribute.
However, that's slightly tricky because attr_reader et al are private class methods so you can't use them directly, and you can't (because it's private) just say self.class.attr_reader, you have to open up Object and do it...with these changes your program works...
#data = { :student => { :id => '123477'} }
class Student
end
s = Student.new
def s.id o
o.data[:student][:id]
#how can I access the #data variable here I tried #data[:student][:id] doesnt work
#I realize that data is outside of the scope of this method. However, is there any way!
end
class Object
attr_reader :data
end
puts s.id self
First off, your id method actually has to go into the class.
You could try something like this:
#data = { :student => { :id => '123477'} }
class Student
attr_accessor :id
def initialize(student)
self.id = student[:id]
end
end
s = Student.new(#data[:student])
puts s.id
#!/usr/bin/ruby
#data = { :student => { :id => '123477', :first_name => 'Lazlo', :last_name =>'Fortunatus', :email=>'Lazlo#fortunatus.org' }, :contact_info => { :telephone=>'1 415 222-2222', :address => '123 Main St', :city =>'Beverly Hills', :state=>'California', :zip_code=>90210, :social_security_number =>'111-11-1111' } }
class Student
def initialize( data )
#data = data
end
def get_id_override
#data[:student][:id]
end
def get_first_name_override
#data[:student][:first_name]
end
def get_last_name_override
#data[:student][:last_name]
end
def get_email_override
#data[:student][:email]
end
def get_telephone_override
#data[:contact_info][:telephone]
end
def get_city_override
#data[:contact_info][:city]
end
def get_state_override
#data[:contact_info][:state]
end
def get_zip_code_override
#data[:contact_info][:zip_code]
end
def get_social_security_number_override
#data[:contact_info][:social_security_number]
end
end
s = Student.new(#data)
def s.id
get_id_override
end
def s.first_name
get_first_name_override
end
def s.last_name
get_last_name_override
end
def s.email
get_email_override
end
def s.contact_info
get_telephone_override
end
def s.city
get_city_override
end
def s.state
get_state_override
end
def s.zipcode
get_zip_code_override
end
def s.ssn
get_social_security_number_override
end
puts s.id
puts s.first_name
puts s.last_name
puts s.email
puts s.contact_info
puts s.city
puts s.state
puts s.zipcode
puts s.ssn
Here is the answer after some work. Anyone has a better answer than mine let me know.
You really should be passing in the data object so it s has its own reference to it.
#data = { :student => { :id => '123477'} }
class Student
attr_accessor :data
def initialize(data)
#data = data
end
end
s = Student.new(#data)
# or you can use s.data = #data
def s.id
#data[:student][:id]
end
puts s.id
A word of caution. Any modifications to #data in the outermost scope will be reflected in s, because both #data variables point to the same object in memory.
But what if you don't want to modify the Student class? You can just add the accessor to s:
#data = { :student => { :id => '123477'} }
class Student
end
s = Student.new
class << s
attr_accessor :data
end
def s.id
#data[:student][:id]
end
s.data = #data
puts s.id
This code does the equivalent of your own answer, with some improvements. (Only by reading that did I realize what you were trying to accomplish.) To avoid being overly complex, I tried to avoid dynamically generating method names.
#!/usr/bin/env ruby
require 'forwardable'
#data = {
:student => {
:id => '123477',
:first_name => 'Lazlo',
:last_name =>'Fortunatus',
:email=>'Lazlo#fortunatus.org'
},
:contact_info => {
:telephone=>'1 415 222-2222',
:address => '123 Main St',
:city =>'Beverly Hills',
:state=>'California',
:zip_code=>90210,
:social_security_number =>'111-11-1111'
}
}
class ContactInfo
def initialize( data )
#data = data
end
def get_telephone_override
#data[:telephone]
end
def get_city_override
#data[:city]
end
def get_state_override
#data[:state]
end
def get_zip_code_override
#data[:zip_code]
end
def get_social_security_number_override
#data[:social_security_number]
end
end
class Student
extend Forwardable # enables delegation (see ruby-doc.org's standard library)
# delegates multiple methods to #contact_info, so they can be called on Student.
# Remember to have the leading colon.
def_delegators :#contact_info,
:get_telephone_override,
:get_city_override,
:get_state_override,
:get_zip_code_override,
:get_social_security_number_override
def initialize( data )
#data = data[:student]
# this is an example of composing objects to achieve separation of concerns.
# we use delegators so ContactInfo methods are available on the Student instance.
#contact_info = ContactInfo.new(data[:contact_info])
end
def get_id_override
#data[:id]
end
def get_first_name_override
#data[:first_name]
end
def get_last_name_override
#data[:last_name]
end
def get_email_override
#data[:email]
end
end
s = Student.new(#data)
class << s
alias_method :id, :get_id_override
alias_method :first_name, :get_first_name_override
alias_method :last_name, :get_last_name_override
alias_method :email, :get_email_override
alias_method :contact_info, :get_telephone_override
alias_method :city, :get_city_override
alias_method :state, :get_state_override
alias_method :zipcode, :get_zip_code_override
alias_method :ssn, :get_social_security_number_override
end
puts s.id
puts s.first_name
puts s.last_name
puts s.email
puts s.contact_info
puts s.city
puts s.state
puts s.zipcode
puts s.ssn
I think your question would've been clearer if you posted the code as you wanted it to work. I'm going to suggest an edit.
Should you be defining an instance variable (prefixed by "#") outside of a class definition?
Also, you can't define a method with a period in the name