Is it possible to verify the number of methods assigned to a Proc in Ruby? - ruby

I'm currently working on a DSL in relation to accounting. What I'd like to be able to do is:
accountant do
credit #account_1, -#amount
debit #account_2, #amount
end
Currently, this executes the following method:
class Accountant
def accountant &block
AccountantHelper.class_eval(&block)
end
end
...Which in turn executes the block on the AccountantHelper class, calling the "credit" and "debit" methods respectively:
class AccountantHelper
def self.credit account, amount
account.credit amount
end
def self.debit account, amount
account.debit amount
end
end
(Please hold back any fire about using class_eval() -- this is only a prototype after all!)
The goal is for the block to act as a transaction, ensuring that if the entire block can't be executed successfully, then none of it should. However in addition to this, it should also verify the integrity of the data passed into the block. In this case I need to verify that there is both a "credit" and a "debit" method within the block (in double-entry accounting, for every credit there must also be at least one debit, and vice versa). Currently I could call:
accountant do
credit #account_1, #amount
end
...And the code will execute without any errors. This would be a bad thing as there is no corresponding "debit" to keep the accounts in balance.
Is it possible to verify what gets passed into the block? Or am I heading down the wrong path here?

I guess you can make your credit and debit actions "lazy", so that they are executed by the wrapper method, after the validation. Here's a proof of concept, similar to yours, but without metaprogramming part, skipped for clarity:
def transaction
yield
if #actions.map(&:last).inject(&:+) == 0
#actions.each do |account, amount|
#accounts[account] += amount
end
#actions = []
puts 'transaction executed ok'
else
puts 'balance not zero, rolling back transaction'
# rollback (effectively, do nothing)
end
end
def credit account, amount
#actions << [account, amount]
end
def debit account, amount
#actions<< [account, -amount]
end
#actions = []
#accounts = {a: 0, b: 0, c: 0} # start with three blank accounts
transaction do
credit :a, 5
debit :b, 2
debit :c, 3
end
#=>transaction executed ok
p #accounts
#=>{:a=>5, :b=>-2, :c=>-3}
transaction do
credit :a, 5
debit :b, 4
end
#=> balance not zero, rolling back transaction
p #accounts
#=> {:a=>5, :b=>-2, :c=>-3}

Related

Turning a single file into MVC without Rails

I need to take the single file code below and separate it into a Model, View, Controller (MVC) ruby program that can run by the ruby command in the command line without using Rails (for instructions on how to run this program from irb, check out the README.md on my RubyBank Github Repo).
require_relative 'view'
class BankAccount
attr_accessor :name, :balance
def initialize(name, balance=0)
#name = name
#balance = balance
end
def show_balance(pin_access)
if pin_access == pin || pin_access == bank_manager
puts "\nYour current balance is: $#{#balance}"
else
puts pin_error_message
end
end
def withdraw(pin_access, amount)
if pin_access == pin
#balance -= amount
puts "'\nYou just withdrew $#{amount} from your account. \n\nYour remaining balance is: $#{#balance}\n\n"
else
puts pin_error_message
end
if #balance < 0
#balance += amount
return overdraft_protection
end
end
def deposit(pin_access, amount)
if pin_access == pin
#balance += amount
puts "\nYou just deposited $#{amount} into your account. \n\nYour remaining balance is: $#{#balance}"
else
puts pin_error_message
end
end
private
def pin
#pin = 1234
end
def bank_manager
#bank_manager = 4321
end
def pin_error_message
puts "Invalid PIN number. Try again."
end
def overdraft_protection
puts "\nYou have overdrafted your account. We cannot complete your withdrawl. Please deposit money before trying again. \n\nYour corrected balance is $#{#balance}"
end
end
I am looking for a good place to start or a general approach towards taking on such a task.
A simple approach would be to create three classes:
BankAccount minus text output is your Model.
All the text I/O goes into your View. Prompt the user for an action or registration. Get the model (for displaying data) from your controller or use the model directly.
Your Controller is responsible for a) reacting to user input, b) modifying the model and c) for holding state not directly related to the BankAccount (this point is discussable) like being logged in or what actions are possible from your current state. Your Controller receives all actions with user supplied data from your view.
Clean separation between View and Controller may be a bit hard in a console application. Also, there are about a million possible ways to implement this in a MVC style. Most important point: no UI-Code (puts/gets) in your model.

RSpec Expectations and ExpectationNotMetError

Hello I have been following a course to further my understanding of cucumber. The course is a little old so i have had to update some rspec syntax from should to expect but otherwise have been following it very carefully. I cannot get this recent test to pass and i am rather lost as to why. The error i am receiving is below. I understand it is receiving nil when it should be receiving 95 but i still do not understand why.
When They submitted an assignment # features/step_definitions/teacher_grade_assignment.rb:6
Then the assignment has a grade # features/step_definitions/teacher_grade_assignment.rb:14
expected: 95
got: nil
(compared using ==)
(RSpec::Expectations::ExpectationNotMetError)
./features/step_definitions/teacher_grade_assignment.rb:15:in `/^the assignment has a grade$/'
features/teacher_can_grade_assignment.feature:11:in `Then the assignment has a grade'
Failing Scenarios:
cucumber features/teacher_can_grade_assignment.feature:7 # Scenario: Teacher can grade assignment
teacher_grade_assignment.rb
Given(/^I have a student$/) do
#teacher = Teacher.new
#student = Student.new
end
Given(/^They submitted an assignment$/) do
#teacher.submit_assignment(#student, Assignment.new)
end
When(/^I grade the assignment$/) do
#teacher.record_grade(#student, 95)
end
Then(/^the assignment has a grade$/) do
expect(#teacher.assignment_for_student(#student).grade).to eq(95)
end
student_assignment_steps.rb
Given(/^I am a student$/) do
#student = Student.new #setting as an instance variable to access later on
#teacher = Teacher.new
end
When(/^I submit an assignment to my teacher$/) do
#assignment = Assignment.new
#teacher.submit_assignment(#student, #assignment)
end
Then(/^my teacher should have my assignment$/) do
expect(#teacher.assignment_for_student(#student)).to eq(#assignment)
end
teacher.rb
class Teacher
def initialize
#assignments = {}
end
def record_grade(student, grade)
assignment = #assignments[student] #assignment equal to assignments of the student
assignment.grade = grade
#assignments[student] = assignment
end
def submit_assignment(student, assignment)
#assignments[student] = assignment
end
def assignment_for_student(student)
#assignments[student]
end
end
teacher_spec.rb
require_relative "../lib/teacher"
require "rspec"
describe Teacher do
it "should store assignments" do
student = double
assignment = double
subject.submit_assignment(student, assignment)
expect(subject.assignment_for_student(student)).to eq(assignment)
end
describe "should record a grade" do
it "should record and the grade" do
student = double
assignment = double
expect(assignment).to receive(:grade=).with(95)
subject.submit_assignment(student, assignment)
subject.record_grade(student, 95)
end
end
end
assignment_spec.rb
require_relative "../lib/assignment"
describe Assignment do
it "should store a grade" do
subject.grade = 60
expect(subject.grade).to eq(60)
end
end
assignment.rb
class Assignment
attr_accessor :grade
end
student.rb
class Student
end
teacher_can_grade_assignment.feature
Feature: Teacher can grade assignment
As a Teacher
I can grade my students' assignments
So that they can know their knowledge level
Scenario: Teacher can grade assignment
Given I have a student
And They submitted an assignment
When They submitted an assignment
Then the assignment has a grade
Your assignment has no grade because the teacher never graded it: your feature doesn't call the "I grade the assignment step"
Your tests are telling you that #teacher.assignment_for_student(#student).grade is nil. Somehow it is not set up correctly. This is unexpected as you state, you expected it to be 95.
In TDD the best next step is to figure out more of the state in the failing test.
Add some extra (temporary) expectations to see what the state of your objects is in the failing step:
expect(#teacher.assignment_for_student(#student)).to eq(#assignment)
expect(#teacher.class_variable_get(:#assigments).to include(#assignment)
Even tests that you are sure will fail can often help a lot.
expect(#teacher.assignment_for_student(#student)).to be "failure"
By sprinkling such expectations around your steps, you can debug the state and see where the code is integrated wrong.
From your pasted code, I don't see anything wrong with the code immediately.

Learnstreet Ruby Lesson 11.5 Transfer state between objects

I've been stuck on this Learnstreet lesson for a day now. The exercise prompts:
Can you now implement a method called transfer! which takes two parameters, amount and other_account. The method should withdraw the specified amount from the current object and deposit it into other_account object.
The code in the editor goes as follows:
class BankAccount
attr_accessor :name, :balance, :address
def initialize(name, balance, address)
#name = name
#balance = balance
#address = address
end
def withdraw!(amount)
if #balance - amount > 0
#balance = #balance - amount
end
#balance
end
def deposit!(amount)
#balance += amount
end
# your code here
end
alices_account = BankAccount.new("Alice Cooper", 2500, "456 University Avenue")
bobs_account = BankAccount.new("Bob Ventura", 2100, "3500 Fox Street")
I know that you need to set up a method with def transfer!(amount, other_account). However I do not know what to put in the bottom after alices_account and bobs_account.
You'd call transfer! on one of the objects, passing in the other, e.g.,
bobs_account.transfer!(500, alices_account)
You're just calling a method on an instance, like "foo".size or [1, 2, 3].each etc. The only difference is that you've created the method you're calling.
I know that you need to set up a method with def transfer!(amount, other_account).
So basically you have to create BankAccount#transfer! that withdraw some money from the object that calls it and deposit the sum into the "other" BankAccount object.
The solution is pretty trivial since you have the BankAccount#withdraw! and BankAccount#deposit! already set up:
def transfer!(amount, other_account)
withdraw! amount
other_account.deposit! amount
end
However I do not know what to put in the bottom after alices_account and bobs_account.
The exercise doesn't require you to do anything with the latter. If you were supposed to do something you would need to know the amount of "money" to transfer from alices_account to bobs_account an viceversa and then go with:
# assuming x to be the amount to transfer
alices_account.transfer! x, bobs_account
or:
# assuming x to be the amount to transfer
bobs_account.transfer! x, alices_account
Ok now. I've spent an hour to complete all the 10 course before that one and this is what I discovered. At some point you get to write the last two lines of your code.
Then a weird thing happens. The code generated by the exercise contains a . To near the end which is obviously a syntax error. By removing that line and adding the method I provided above you get to pass the test.

Undefined local variable or method 'product'

I am doing a task that requires me add some products together and give a 10% discount providing the total is above £60. I have done the following:
class Checkout
def initialize (rules)
#rules = rules
#cart = []
end
def scan (item)
if product == Product.find(item)
#cart << product.clone
#Clone preserves frozen state whereas .dup() doesn't if use would raise a
#NoMethodError
end
end
def total
#cart = #rules.apply #cart
end
def self.find item
[item]
end
co = Checkout.new(Promotional_Rules.new)
co.empty_cart
co.scan(1)
co.scan(2)
co.scan(3)
puts "Total price: #{co.total}"
puts
co.empty_cart
co.scan(1)
co.scan(3)
co.scan(1)
puts "Total price: #{co.total}"
puts
co.empty_cart
co.scan(1)
co.scan(2)
co.scan(1)
co.scan(3)
puts "Total price: #{co.total}"
puts
However when I run this in irb I get undefined variable or method product. Sounds a bit daft but this should work.
You're using one too many equal signs
def scan (item)
# if product == Product.find(item)
if product = Product.find(item) # <--- should be this
#cart << product.clone
#Clone preserves frozen state whereas .dup() doesn't if use would raise a
#NoMethodError
end
end
Of course, then you'll get a different error since find doesn't exist on Product yet... which I think you're trying to define here:
def self.find item # self should be changed to Product
[item]
end
Then you're going to get an error for apply not existing for Promotional_Rules ...
One of the best ways to debug these errors is follow the stack traces. So for the last error I get the following message:
test.rb:53:in `total': undefined method `apply' for #<Promotional_Rules:0x007f94f48bc7a8> (NoMethodError)
from test.rb:72:in `<main>'
That's basically saying that at line 53 you'll find apply hasn't been defined for #rules which is an instance of Promotional_Rules. Looking at the Promotional_Rules class you've clearly defined that method as apply_to_item and not apply. If you keep following and fixing the rabbit trails like this for stack traces you'll be able to debug your program with ease!

How display failure on undescribed example in RSpec?

I am describing class on RSpec
class Pupil
def initialize(name, dateOfBirth)
#name = name
#dateOfBirth = dateOfBirth
end
def name
#name
end
def ages
#should calculate ages
end
end
describe Pupil do
context do
pupil = Pupil.new("Stanislav Majewski", "1 april 1999")
it "should returns name" do
pupil.name.should eq("Stanislav Majewski")
end
it "should calculates ages" do
#not described
end
end
end
RSpec returns:
..
Finished in 0.00203 seconds
2 examples, 0 failures
Is there an elegant way to display a failure message that the method is not described?
If you're concerned that you'll create a test and forget to put anything it in (sometimes I'll create three tests I know I'll need, and work on each of them in turn) then you can do the following:
it "should calculates ages" do
fail
end
OR
it "should calculates ages"
...and that's all (no block) will mark the test as pending automatically. In other words, don't fill out your tests until they have actual test code in them.
Also, if you don't test any assertions (i.e. if your spec doesn't contain any lines that have a call to should in them), your spec will appear to pass. This has happened to me a few times, where I write a new test, expecting it to fail, and it doesn't because I forgot to include the call to should which is what actually tests the assertion.

Resources