28-01-2022

3514 words 18 minutes Ruby User Stories

Meta Stuff

config, updates, installs, software, etc


Today's Foci

Focus 1

  1. What/How?
  2. Measure

If I'm with others ask how productive they want to be, and determine how chatty I should be in return

  • ask 1 / 10?
  • what are you goals in this session?

Topic

Challenge! Which one?

Both have twilio / text stretch goals appear to use a twilio gem 'twilio-ruby'

Use Mocks/stubs so that actual texts aren't sent

Use ruby environment variables


The office management has 10 simple user stories

MeetingRoomOBJ with

  • attributes @name:String, @available:Bool, methods
  • check_room return @available
  • enter_room @available = false - Raise error if entering occupied room
  • exit_room @available = true

OfficeOBJ with

  • attributes @listrooms:Array

  • methods add_room @listrooms push MeetingRoomOBJ

  • available @listrooms while MeetingRoomOBJ.available == true

ManagementInterfaceOBJ ( uses twilio API )

  • attributes

  • methods

  • room_free ? return array of rooms or a single room? maybe both? room x is free here is a list of rooms? goes beyond User Story, but I would intuit that is what the client wants.

    • while array @listrooms, if element ( MeetingRoomOBJ ).exit_room, activate twilio API, sending MeetingRoom, and list of available rooms?
  • room_occupied returns from @listrooms, while MeetingRoom.available==false, return MeetingRoomOBJ.name and MeetingRoomOBJ.team?

?

list of teams or team object? see how much time I have I guess?

TeamOBJ (used for entering rooms) attribute name:String?

if so, modify the MeetingRoomOBJ.enter and .exit methods to take a Team object and return the TeamOBJ.name


I think the takeaway challenges are a little more than meets the eye - but there are only 4 of them. Hmmm.

I'm leaning toward the takeaway challenge

takeaway - array of hashes for dish=>price

from above array, remove items

show array.val(returns price), individually and array.sum

receive a text to confirm the order has been placed and a time (datetime + 30 mins?)


30 mins down, rough plan - Office Task decided

Office seems more interesting!

Before anything, lets use the Apprenticeship Providers advice and make use of their fantastic tables! (hidden somewhere in my resources file...)

User Stories

As a staff member
In order to distinguish between meeting rooms *names must be unique?*
I would like my meeting room to have a name
As an office manager
So that staff can coordinate meetings
I would like to add a meeting room to the office
As an office manager
So that I can manage meeting rooms
I would like to list all the meeting rooms in the office
As a staff member
In order to have meeting,
I would like to check if the meeting room is available or not (true or false)
As a staff member
In order to have a meeting,
I would like to be able to enter the meeting room and this should make it unavailable
As a staff member
In order to end a meeting
I would like to be able to leave the meeting room and this should make it available again
As a staff member
So that I can see where to have my meeting
I would like to be able to see a list of available rooms in the office
As a staff member
So that I can avoid interrupting a meeting
I would like an error if I try to use a room that has already been entered
As an office manager
So that I can find out when a room becomes available
I would like to receive a text message whenever a meeting room becomes available again
As an office manager
So that I can have visibility of how the rooms are being used
I would like to see the name of the meeting and the name of the team that is using the room

Voila!

Diagram Modelling - OOP

NounVerbProp?
MeetingRoom
name
occupied
enter_room
exit_room
Office
room_list
add_room
list_all_rooms
list_available_rooms
ManagerInterface
receive_message
meeting_details
?Team
name
meeting_name

I feel the table below is something I've alreaady covered, but I will continue regardless - perhaps I will find something I've missed?

NounProperty or Owner?
MeetingRoomOwner
MeetingRoom.nameProperty
MeetingRoom.occupiedProperty
OfficeOwner
Office.room_listProperty
text_messageProperty (of what)?
?Team.nameProperty?
Team.meeting_agendaProperty

Having done the above, i feel the first table covered a lot of ground - it describes the actions and properties of an object, and its owner. It's like an informal UML. I'm gonna skip the next one, but maybe look into the propertys they change?

ActionsOwned by
ActionsProperty it Reads or Changes
MeetingRoom.enter_roomoccupied
MeetingRoom.exit_roomoccupied
Office.add_roomroom_list
Office.list_all_roomsroom_list
Office.list_available_roomsroom_list where MeetingRoom.occupied == true
ManagerInterface.receive_messageMeetingRoom.exit_room ( and Office.list_available_rooms? )
ManagerInterface.meeting_detailsOffice.list_rooms - Office.list_available_rooms? (maybe works because of Ruby arrays? will see), Team.name, Team.meeting_agenda

Again, table one has done most of the legwork - ( straight to UML next week maybe? ) don't think I need the below

ClassCLASSNAME
Properties (instance variables)
Actions (methods)

It's been about an hour and a half 10:52, break til 11


TDD through the User Stories

Commit after each successful test!

but first GH, fork the repo local, clone MY GH fork check Gemfile for ruby version 3.0.0 > 3.0.2, ace! run bundle

add twilio-ruby in advance, less to think of later tbh rubygems.org, 5.63.1 as of Jan 26 2022 - brilliant places in :test AND :development groups in Gemfile - I think that's correct - test for the RSpec, and development will give accesswhilst running in a REPL

check git log

bundle appears to have worked!


User Story 1

As a staff member
In order to distinguish between meeting rooms *names must be unique?*
I would like my meeting room to have a name

I might as well check for unique names if possible, right?...

./spec/meetingroom_spec.rb

require './lib/meetingroom'

describe MeetingRoom do
  let(:room) { MeetingRoom.new("some_room") }

  it '#has a name' do
    expect(room.name.class).to be String
  end
end

./lib/meetingroom.rb

I chose the accessor because its reasonable that an office may want to change the name of a meeting room. What security features does this break / bend?

class MeetingRoom

  attr_accessor :name

  def initialize(name)
    @name = name
  end
end

User Story 2

As an office manager
So that staff can coordinate meetings
I would like to add a meeting room to the office

I need to create an office class, with a room_list instance variable of type Array. It must have a method to take a MeetingRoom, and push it to the room_list Array.

How do I test for this? that a room has been pushed to the in the array.

I will try using a double - its not 100% necessary, but is good practice. if it works, huzzah!

./spec/office_spec.rb

require './lib/office'

describe Office do
  let(:office) { Office.new }
  let(:room1) { double(name: "Azure", occupied: false) }

  it '#add\'s meeting rooms' do
    office.add_room(room1)

    expect(office.room_list).to include(room1)
  end
end

remember - as 'git commit -am' adds and commits

NERDTree - 'ma' will make a new file/subdirectory in highlighted directory

Failures

  1. LoadError, no file
  2. Forgot to make the attr_reader
  3. Gave an argument to the instance variable - derp!
class Office

  attr_reader :room_list

  def initialize
    @room_list = []
  end

  def add_room(room)
    @room_list << room
  end
end

Success, green and the double worked ( i think as intended? ) happy days!


User Story 3 4.0 - whoops!

As a staff member
In order to have meeting,
I would like to check if the meeting room is available or not (true or false)

A meeting room shall be given the instance variable @occupied, with a boolean value. default is true. It is not constructor argument - all meeting rooms at creation would surely be false.

how do I check for this?

expect MeetingRoom.occupied to return true or false, i think

./spec/meetingroom_spec.rb

it '#it\'s occupied or not' do
	expect(room.occupied).to be(true).or be(false)
end

./lib/meetingroom.rb

class MeetingRoom

  attr_accessor :name
  attr_reader :occupied

  def initialize(name)
    @name = name
    @occupied = false
  end
end

we don't want to change the meeting room occupied from outside the meeting room - so it only has a reader

Errors

  1. NoMethodError - expected
  2. Error in RSpec - false not true - i wrote the code wrong

Fixed the error, and it worked!


Interim

I forgot to make the MeetingRoom names unique to the office.

test

I will expect a MeetingRoom to raise an error if it is being pushed to the Office.room_list array, if it's already present

./spec/office_spec.rb

  it '#won\'t add room, if already in room_list' do
    office.add_room(room1)
    p room1.name

    expect { office.add_room(room1) }.to raise_error "A meeting room of the same name exists within this office. Please choose another name for the meeting room."
  end

./lib/office.rb

  def add_room(room)
	return raise ROOM_EXISTS_ERROR if @room_list.include?(room)

	@room_list << room
  end

User Story 4.1

I'm going to change my plan somewhat. According to my plan, I think staff would physically check the meeting room, in order to see if it's occupied.

Now, were I in the Gherkin, I wouldn't want to go through every office and knock on every door, to check for occupancy. I'd ask the front desk.

I'd want to check them against all meeting rooms the office has. ( If I were to extend this, i'd perhaps add a datetime object to an occupied room at clock in, expected-clockout, and actual-clockout )

test

expect the function Office.check_room to return a true or false value from MeetingRoom.occupied

./spec/office_spec.rb

  it '#checks room occupancy' do
    office.add_room(room1)

    expect(office.check_room(room1)).to eq("#{room1.name} is available").or eq("#{room1.name} is unavailable")
  end

failures

  1. NoMethodError

./lib/office.rb

  def check_room(room)
    return "#{room.name} is available" if room.occupied == false

    "#{room.name} is unavailable"
  end

worked first time!

is this worrisome because room.occupied could have a truthy value, rather than explicitly being the Boolean, true?


Interlude 2 Gondor calls for Aid!

So this is a solo challenge.

However

Some were stuck on some stuff - some fundamental stuff to do with classes, instantiation, the main programme, the testing environment AND the unit tests.

My intention was to only push in the right direction - I think I did so.

And I think we all made good progress. It was productive for everyone. Step throughs, line by line, experimenting together.

I felt at the present time, it was the right thing to do. I feel in hindsight it was the right thing to do. It was neither copying, nor telling people what to write. I'm not a compsci tutor, but I had to try. Together, we prevailed - we didn't add much, but we did refactor to make everything function, AND to be able to explain how and why things were working.

Immediately after, I saw the group use this knowledge to create their own answer for the next test, without my aid (other than my incessant and insufferable questioning) as to whether a string is its' own object. There was tangible progress.

If there are repercussions, well... Probably there will be. But I made the right choice.

The hour grows late - but code - code never sleeps!


User Story 3 - actually 3, this time haha!

As an office manager
So that I can manage meeting rooms
I would like to list all the meeting rooms in the office

We want to check that the @room_list instance variable returns all rooms

Hmm, the test was green - by giving the room_list an attr_reader, this sort of anticipated this question

I added the test below, but feel its a little redundant

Perhaps, if I have wish, I should try and make a factory to construct multiple rooms for me? Then use that to look for an error. Use the factory to run the feature test, etc.

I added the test below - to be honest, it feels redundant - I've already covered this? perhaps I should change the getter to a function? I don't know.

  let(:room2) { double(name: "e3", occupied: true) }

...

  it '#lists all rooms' do
    office.add_room(room1)
    office.add_room(room2)

    expect(office.room_list).to include(room1).and include(room2)
  end

User Story 5 and 8

As a staff member
In order to have a meeting,
I would like to be able to enter the meeting room and this should make it unavailable

given that this will require a guard statement, I'm going to also add User Story 8 here

As a staff member
So that I can avoid interrupting a meeting
I would like an error if I try to use a room that has already been entered

Referring back to my tables above, I need to create an enter_room method which changes the MeetingRoomINST.occupied to false

check_room should return "#{MeetingRoomINST} is unavailable}"

so my test should first change change MeetingRoomINST via the enter_room method, and then check MeetingRoomINST.occupied == false?

I can add a guard clause to raise an error if the MeetingRoomINST.occupied is true

./spec/meetingroom_spec.rb

  it '#is occupied when entered' do
    room.enter_room
    expect(room.occupied).to be true
  end

  it '#raises error if accessing room in-use' do
    room.enter_room

    expect { room.enter_room }.to raise_error "room in use"
  end

Failures

  1. NoMethodError

./lib/meetingroom.rb

  def enter_room
    return raise "room in use" unless @occupied == false

    @occupied = true
  end

User Story 6

As a staff member
In order to end a meeting
I would like to be able to leave the meeting room and this should make it available again

again this is a method on the MeetingRoomINST

I will be testing that, once occupied, a MeetingRoomINST.exit_room will make MeetingRoomINST.occupied == false

./spec/meetingroom_spec.rb

  it '#is unoccupied when exited' do
    room.exit_room

    expect(room.occupied).to be false
  end

Failures

  1. NoMethodError
def exit_room
	@occupied = false
end

User Story 7

As a staff member
So that I can see where to have my meeting
I would like to be able to see a list of available rooms in the office

Looking back at my plan, the Office class needs a method 'list_available_rooms'

im expecting in the code to do a kind of array.do | ele | and run ele.occupied, to return an array containing a list of rooms which are available

in the test, I want a OfficeINST.list_available_rooms to return an array that does include an room.occupied == false, and excludes room.occupied == true - can I do this in the test using RSpecs include and maybe !include?o

looking at the RSpec docs, I would need to use .not_to include(MeetingRoomINST) - fantastic!

oooo found this here! unsure if i need the second line - once green I can test and remove as appropriate

RSpec::Matchers.define_negated_matcher :not_include, :include
RSpec::Matchers.define_negated_matcher :not_eq, :eq

I'm guessing that RSpec method .define_negated_matcher takes 2 args; arg2 looks like the original symbol, whilst arg1 is the negated derivision of arg2?

refactored office_spec vars more clearly - room_unoccupied == room1, room_unoccupied == room2

./spec/office_spec.rb

require './lib/office'

RSpec::Matchers.define_negated_matcher :not_include, :include
RSpec::Matchers.define_negated_matcher :not_eq, :eq

describe Office do
  let(:office) { Office.new }
  let(:room_unoccupied) { double(name: "Azure", occupied: false) }
  let(:room_occupied) { double(name: "e3", occupied: true) }

  ...

  it '#can list only the available rooms' do
    office.add_room(room_unoccupied)
    office.add_room(room_occupied)

    expect(office.list_available_rooms).to include(room_unoccupied).and not_include(room_occupied)
  end

Failures

  1. NoMethodError
Failures:

  1) Office #can list only the available rooms
     Failure/Error: expect(office.list_available_rooms).to include(room_unoccupied).and not_include(room_occupied)

          expected [#<Double (anonymous)>] to include #<Double (anonymous)>

       ...and:

          expected [#<Double (anonymous)>] not to include #<Double (anonymous)>
       Diff for (include #<Double (anonymous)>):
         <The diff is empty, are your objects producing identical `#inspect` output?>
       Diff for (not include #<Double (anonymous)>):
         <The diff is empty, are your objects producing identical `#inspect` output?>
     # ./spec/office_spec.rb:40:in `block (2 levels) in <top (required)>'

Finished in 0.02235 seconds (files took 0.10873 seconds to load)
10 examples, 1 failure

Failed examples:

rspec ./spec/office_spec.rb:36 # Office #can list only the available rooms

Well!... that's a new one! something to do with the doubles. They're not attached to the MeetingRoom classes. I think they're spies?... Hmmm...

I p'd out the list_available_rooms, and maybe it worked - there's only 1 item within. But it returns the anonymous double. Hmmm.

Ah. I gave my doubles names, to stop them being anonymous; then I p'd out my OfficeINST.list_available_rooms, only to find, everything was working opposite of how I'd expected. Mainly because I was adding only the unavailable rooms. Whoops!

  let(:room_unoccupied) { double("Azure", name: "Azure", occupied: false) }
  let(:room_occupied) { double("e3", name: "e3", occupied: true) }

Changed the true value to false, and all is well

./lib/office.rb

  def list_available_rooms
    available_rooms = []
    @room_list.each do |room|
      if room.occupied == false
        available_rooms << room
      end
    end
    available_rooms
  end

let's experiment with the RSpec Matcher a little - hahahaaha, wonderful! works as i'd guessed

RSpec::Matchers.define_negated_matcher :cats_for_brains, :include

...

    expect(office.list_available_rooms).to include(room_unoccupied).and cats_for_brains(room_occupied)

I'll commit that with the cats_for_brains, and change it back before the next commit, and nobody need ever know... ;)


User Story 9 - Twilio

this might be difficult. I'm anticipating environment variables, which I can store in .env - how will i access those? a gem? or simply require them in? i don't know. I need to look at best practices first.

I also need to check a twilio account - i think I might have one already? I'm uncertain. Let's do that first.

ugh, I got caught refactoring the last user Story... instead of a whole block i can use .select, i think, but I need the opposite of .select?...

god dayuuummm, sometimes I'm too good, or at least, the ruby resources are, hahah!

./lib/office.rb

before

  def list_available_rooms
    available_rooms = []
    @room_list.each do |room|
      if room.occupied == false
        available_rooms << room
      end
    end
    available_rooms
  end

after

  def list_available_rooms
    @room_list.reject(&:occupied)
  end

the above returns a new array, where the argument evaluates to true or false statement evaluates to false

User Story 9, proper

As an office manager
So that I can find out when a room becomes available
I would like to receive a text message whenever a meeting room becomes available again

I think it's time to make the class ManagerInterface and it's spec. hmmm.o

Right, so, looking at the twilio API, the first thing I wanted to do was

add .env to my .gitignore

sign up to twilio and get the SID and auth_token

make a .env file, and add SID and auth_token

add dotenv from rubygems.org to my Gemfile

run bundle

it worked!

then use

dotenv -t .env

to have the dotenv Gem make a template of my .env file at .env.template, which I can push to git as an example - ace!


time to make the twilio account work

so I think I have the code and the twilio account set up - I should receive a JSON when I run some code - I hope? fingers crossed.

need to require the ManagerInterface in the MeetingRoom file

when MeetingRoomINST.exit_room, this code in ManagerInterfaceINST.receive_sms will run

so I'm going to expect the ManagerInterfaceINSTJson to maybe call another method that parses the JSON data?

ugh this is getting complicated hahaha and its 22:30. hmm. No rest for the wicked.

in which case, I'm expecting ManagerInterfaceINST.receive_sms to receive a string saying "#{room} is available"

./spec/managerinterface_spec.rb

require './lib/managerinterface'

describe ManagerInterface do
  let(:interface) { ManagerInterface.new }
  let(:room_unoccupied) { double("RoomID:001", name: "Hyperion Suite", occupied: false) }

  it '#receives an SMS when meeting room becomes available' do
    expect(interface.receive_sms(room_unoccupied)).to eq "#{room_unoccupied.name} is now available"
  end
end

Failures

  1. many, first was using the dotenvs k/v's as instance variables on the ManagerInterface
  2. Then was a Gemfile issue, installing rack
  3. then a few issues with a phone number
  4. finally, despite the docs telling me i was receiving a JSON, I believe the twilio-gem does some magic to provide me access via method calls such as 'message.body' below

./lib/managerinterface.rb

require 'twilio-ruby'
require 'dotenv/load'

class ManagerInterface

  def initialize
    @account_sid = ENV['TWILIO_ACCOUNT_SID']
    @auth_token = ENV['auth_token']
    @number = ENV['PHONE_NUMBER']
  end

  def receive_sms(room)
    @client = Twilio::REST::Client.new @account_sid, @auth_token

    message = @client.messages.create(
      body: "#{room.name} is now available",
      to: @number,
      from: '+15005550006',
    )

    message.body
  end
end

Okay, so this one is a little rough. How do I do this? I could jam the receive_sms method into the MeetingRoom.exit_room class; but that would increase the coupling. It would be the simplest solution. However - what I think I want, is an Observer pattern?

But where?

I think to monitor the office, for when a room is exited - but how can I access the MeetingRoom.exit_room method?

in OfficeInst, while office_hours, for room in @room_list, if room.exit_room, ManagerInterfaceINST.receive_text

how does an observer watch the OfficeINST, the ManagerInterfaceINST, and the MeetingRoomINSTS simultaeneously?

ugh, it's past midnight, and fun as this is, it's quickly becoming unweildy, fudge.

hmmm, apparently an observer is a many observers-to-one subject - I want many subjects to one observer.

many rooms, to one manager.

could I get an MeetingRoomINST.exit room to send a message to an Office interface? that might work?

then whenever that office interface is triggered, the ManagerInterfaceINST.receive_sms is triggered, with the parameter of the appropriate room?

I think this starts in the MeetingRoom.exit_room method - how do I send a message to a different class?


A New Day

It is 11AM Saturday morning. And it's time to sort this twilio stuff out.

So:

  • At present, I don't have the knowledge, nor genuine need, to implement some kind of interfacing class between a ManagerInterfaceINST and the MeetingRoomINST.

  • I have a plan, however

  1. Meet the criteria. I'm gonna plug the manager interface object into the MeetingRoom.exit_room method as an argument - it will call the receive room from there. Shoddy, a bit hacky, but it will do for now.

  2. I will implement the 10th User Story, the Team, probably as a class

  3. IF i wish, I will try and do something like:

make a ManagerInterface method

that monitors the states of each element

in an office's room_list array attribute

 for either:
 element.occupied changing from true, to false

 or

 when element.exit_room is called

Onward

Done. Oof. I'm unsure how I feel. It works; but I feel it should work betterer. Gah. Frustration. I can refactor later, and maybe ask for some help also, after I've pushed to remote, and done the pull request.

Anyway - the test.

./spec/managerinterface_spec.rb

  it '#receives an sms when room becomes available' do
    expect(room.exit_room(interface)).to eq "#{room.name} is now available"
  end

Failures

  1. The first error was my not having made a parameter in the MeetingRoomINST.exit_room method.
  2. My second was passing in only @name - the error message showed it was calling the .name method / accessor (are they one and the same in Ruby? I must look into that) on the @name I was passing in. A breath, and I realised the error. Maybe I needed to pass in self?
  3. There was green! But an earlier test was broken - i needed to pass in a parameter there too.

./lib/meetingroom.rb

  def exit_room(interface)
    @occupied = false

    interface.receive_sms(self)
  end

./spec/meetingroom_spec.rb

  it '#is unoccupied when exited' do
    interface = ManagerInterface.new

    room.exit_room(interface)

    expect(room.occupied).to be false
  end

lucky I checked git status there - for some reason, I think because of the .env.template file,

git commit -am "some message"

didn't add the files before commiting.

User Story 10

in all honesty I need to refactor the test as well - they're calling Twilio on a test account - so I'm not charged, but I need to create a dummy message class to call message.body on, or to create a stub. I'll look at that later

because I had a brainwave.

right

Team is gonna be a class. On MeetingRoomINST.enter_room an argument of TeamINST is gonna be taken. TeamINST is gonna have an agenda, and list of team members (maybe a hash where keys are name, role), and an agenda

but lets not get ahead of ourselves - lets grab the User Story

As an office manager
So that I can have visibility of how the rooms are being used
I would like to see the name of the meeting and the name of the team that is using the room

Okay - so my above idea is a little overboard, which is a shame. I need a hash perhaps, where keys are :meeting_name, and :team_name.

I still think a team class is useful.

Similiar to above, TeamINST perhaps passes these into the MeetingRoomINST temporarily, and on exit_room, returns them to nil?

ugh, thats a lot of tests

more than one user story required. Overcomplicated.

it's gonna break a lot of stuff - it's fine, I have git.

let's do this.

  1. Team tests

i want a team to have a team_name i want a team to have a meeting_name

I want these to be accessible - perhaps even an accessor?

maybe I can do this in one test?

i want TeamINST to have attributes team_name AND meeting_name.

./spec/team_spec.rb

require './lib/team'

describe Team do
  let(:team_proton) { Team.new("proton") }

  it '#can set an agenda' do
    expect(team_proton).to respond_to(:set_agenda)
  end

  it '#has attributes name and agenda' do
    team_proton.set_agenda("TypeScript refactor")

    expect(team_proton).to have_attributes(name: 'proton', agenda: "TypeScript refactor")
  end
end

Failures

  1. LoadError, as expected - no team.rb to require
class Team

  attr_reader :name, :agenda

  def initialize(name)
    @name = name
    @agenda = nil
  end

  def set_agenda(agenda)
    @agenda = agenda
  end
end

there we go; but this is becoming complicated, and things are becoming coupled. Fudge. Sigh. I need to understand interfaces.

I'm thinking the Team goes into the meeting room, there is a method that sends that info to the managerinterface, that is called in the manager class.

hmmm. I feel I should be using the office class more to be honest.

from the office_occupied? I could use select?

In ./lib/managerinterface.rb?

def check_team(MeetingRoomINST)
	return raise "room doesn't exist" unless OfficeINST.room_list.include?(MeetingRoomINST)
	return "room empty" if OfficeINST.list_available_rooms.include?(MeetingRoomINST) 

	MeetingRoomINST.meeting_info
end

In ./lib/meetingroom.rb?

def meeting_info
	return "room empty" unless @occupied == true

	"Team #{Team.name} is working on #{Team.agenda}"
end

Perhaps I need to use something like a chain; the meeting room info moves to the office, moves to the manager? that means there are far more functions and tests though. This is definitely some kind of interface class, external to the office/meetingroom/managerinterface/team. aaaggghhhhh!

but then the test will start going crazy? but maybe they should? The classes, should be simple, and the tests should use that logic to build complexity; right?

gah.

I'm feeling class 5 coming on. This is a bad idea.

I haven't even properly implemented class 4.

And aren't I supposed to make an interface for different messaging patterns? So I couldn't use the same one for the SMS and the room info from the manager?

what would it look like?

require './lib/meetingroom'
require './lib/office'

class RoomInfoInterface

	def check_room(OfficeINST, MeetingRoomINST)
		return raise "room doesn't exist" unless OfficeINST.room_list.include?(MeetingRoomINST)
		return "room empty" if OfficeINST.list_available_rooms.include?(MeetingRoomINST)

		MeetingRoomINST.meeting_info
	end
end

This feels overly complicated. The sheer amount of testing I will need to complete for this feature is probably the same as the initial task itself. It's a bad idea.

What's the alternative? Again, the shoddy way, just attach it to the MeetingRoom class. Sigh.

Realities of Coupling

'A common anti-pattern is making an interface that matches what the consumed class offers.'

Right, I think we're doing this. We're gonna write our own "user stories" for the implementation of a RoomInfoInterface.

So that the manager accesses only an interface to gain access to room.meeting_info

  1. interface accesses the office.room_list, to check room exists

  2. interface accesses the office.list_available_rooms to check room is occupied

  3. interface can access the room.meeting_info, from within the office

  4. room has attributes :name, :occupied, :team, :agenda

  5. when a team enters a room, room shares :team and :agenda with teams' :name and :agenda

  6. when a team exits a room, room returns :team and :agenda to nil


let's do 4, 5, and 6 first. They're going as one commit. I'm not doing one test at a time at this point.o

Oof, that was a little tougher but we got there in the end. The tests are becoming bulkier and I don't like it, but hey, what can you do.

./spec/meetingroom_spec.rb

  it '#has teams name and agenda when in use' do
    room.enter_room(team_red)

    expect(room.meeting_info).to eq "Team #{team_red.name} in meeting #{team_red.agenda}"
  end

  it '#team and agenda are nil after exit' do
    room.enter_room(team_red)

    room.exit_room(interface)

    expect(room.check_clear).to eq({ team: nil, agenda: nil })
  end

./lib/meetingroom.rb

require_relative 'managerinterface'

class MeetingRoom

  attr_accessor :name
  attr_reader :occupied

  def initialize(name)
    @name = name
    @occupied = false
    @team = nil
    @agenda = nil
  end

  def enter_room(team)
    return raise "room in use" unless @occupied == false
    
    @occupied = true
    @team = team.name
    @agenda = team.agenda
  end

  def exit_room(interface)
    @occupied = false
    @team = nil
    @agenda = nil

    interface.receive_sms(self)
  end

  def meeting_info
    return "room empty" unless @occupied == true

    "Team #{@team} in meeting #{@agenda}"
  end

  def check_clear
    { team: @team, agenda: @agenda }
  end
end

Implement RoomInfoInterface

  1. interface accesses the office.room_list, to check room exists

  2. interface accesses the office.list_available_rooms to check room is occupied

  3. interface can access the room.meeting_info, from within the office

./spec/roominfointerface_spec.rb

require './lib/office'
require './lib/meetingroom'
require './lib/roominfointerface'

describe RoomInfoInterface do
  let(:info_interface) { RoomInfoInterface.new }
  let(:office) { Office.new }

  let(:room1) { MeetingRoom.new("001") }

  let(:team) { double(name: "Mystery Inc", agenda: "Jinkies!") }

  it '#can access meeting_info' do
    office.add_room(room1)
    room1.enter_room(team)

    expect(info_interface.get_meeting_info(office, room1)).to eq "Team Mystery Inc in meeting Jinkies!"
  end
end

Failures

  1. Well, the first error is pretty standard - there's no associated class ...

There were more failures than mere StandardErrors! At some point TDD gave way to a mix of test-making development. I coudln't figure out how I wanted to test the interface in advance - I suppose I hadn't planned to make one, but such as it is, I have a working interface. I only fell at the last hurdle, and it was a hurdle I willingly chose to go for. Better luck next time!

./lib/roominfointerface.rb

require_relative 'office'
require_relative 'meetingroom'

class RoomInfoInterface

  def get_meeting_info(office, room)
    return raise "room doesn't exist" unless office.room_list.include?(room)
    return "room empty" if office.list_available_rooms.include?(room)

    room.meeting_info
  end
end

There are things I could have done better. Up until the last moment, I think things were going quite well. Will I try refactoring the sms into an interface tomorrow? ugh, I don't know. I'm tired haha. Honestly, the difficult part was refactoring the other classes to work with the interface. I definitely could have done the MeetingRoom / Team classes better. The idea of them temporarily sharing the attributes is useful, but I should have worked something else out, as now the Team class looks messy

Full-Code below


Office Challenge

./spec/office_spec.rb

require './lib/office'

RSpec::Matchers.define_negated_matcher :not_include, :include

describe Office do
  let(:office) { Office.new }
  let(:room_unoccupied) { double("Azure", name: "Azure", occupied: false) }
  let(:room_occupied) { double("e3", name: "e3", occupied: true) }

  it '#add\'s meeting rooms' do
    office.add_room(room_unoccupied)

    expect(office.room_list).to include(room_unoccupied)
  end

  it '#won\'t add room, if already in room_list' do
    office.add_room(room_unoccupied)

    expect { office.add_room(room_unoccupied) }.to raise_error "A meeting room of the same name exists within this office"
  end

  it '#lists all rooms' do
    office.add_room(room_unoccupied)
    office.add_room(room_occupied)

    expect(office.room_list).to include(room_unoccupied).and include(room_occupied)
  end

  it '#checks room occupancy' do
    office.add_room(room_unoccupied)

    expect(office.check_room(room_unoccupied)).to eq("#{room_unoccupied.name} is available").or eq("#{room_unoccupied.name} is unavailable")
  end

  it '#can list only the available rooms' do
    office.add_room(room_unoccupied)
    office.add_room(room_occupied)

    expect(office.list_available_rooms).to include(room_unoccupied).and not_include(room_occupied)
  end
end

./lib/office.rb

class Office

  ROOM_EXISTS_ERROR = "A meeting room of the same name exists within this office".freeze

  attr_reader :room_list

  def initialize
    @room_list = []
  end

  def add_room(room)
    return raise ROOM_EXISTS_ERROR if @room_list.include?(room)

    @room_list << room
  end

  def check_room(room)
    return "#{room.name} is available" if room.occupied == false 

    "#{room.name} is unavailable"
  end

  def list_available_rooms
    @room_list.reject(&:occupied)
  end

end

./spec/meetingroom_spec.rb

require './lib/meetingroom'
require './lib/managerinterface'

describe MeetingRoom do
  let(:room) { MeetingRoom.new("Marylebone Suite") }
  let(:interface) { ManagerInterface.new }
  let(:team_red) { double("Red Leader", name: "Red Ten", agenda: "Standing By") }
  let(:team_rocket) { double("Team Rocket", name: "Team Rocket", agenda: "Blasts off; again...") }

  it '#has attributes name, occupied' do
    expect(room).to have_attributes(name: "Marylebone Suite", occupied: false)
  end

  it '#it\'s occupied or not' do
    expect(room.occupied).to be(true).or be(false)
  end

  it '#is occupied when entered' do
    room.enter_room(team_red)
    expect(room.occupied).to be true
  end

  it '#raises error if accessing room in-use' do
    room.enter_room(team_red)

    expect { room.enter_room(team_rocket) }.to raise_error "room in use"
  end

  it '#is unoccupied when exited' do
    room.exit_room(interface)

    expect(room.occupied).to be false
  end

  it '#has teams name and agenda when in use' do
    room.enter_room(team_red)

    expect(room.meeting_info).to eq "Team #{team_red.name} in meeting #{team_red.agenda}"
  end

  it '#team and agenda are nil after exit' do
    room.enter_room(team_red)

    room.exit_room(interface)

    expect(room.check_clear).to eq({ team: nil, agenda: nil })
  end
end

./lib/meetingroom.rb

require_relative 'managerinterface'

class MeetingRoom

  attr_accessor :name
  attr_reader :occupied

  def initialize(name)
    @name = name
    @occupied = false
    @team = nil
    @agenda = nil
  end

  def enter_room(team)
    return raise "room in use" unless @occupied == false

    @occupied = true
    @team = team.name
    @agenda = team.agenda
  end

  def exit_room(interface)
    @occupied = false
    @team = nil
    @agenda = nil

    interface.receive_sms(self)
  end

  def meeting_info
    return "room empty" unless @occupied == true

    "Team #{@team} in meeting #{@agenda}"
  end

  def check_clear
    { team: @team, agenda: @agenda }
  end
end

./spec/managerinterface_spec.rb

require './lib/managerinterface'
require './lib/meetingroom'
require './lib/office'

describe ManagerInterface do
  let(:interface) { ManagerInterface.new }
  let(:info_interface) { RoomInfoInterface.new }

  let(:room_unoccupied) { double("RoomID:001", name: "Hyperion Suite", occupied: false) }
  let(:room) { MeetingRoom.new("Seychelles Suite") }

  let(:team) { double(name: "The Incredibles", agenda: "Misplaced Super-Suits") }
  let(:office) { Office.new }

  it '#can receive an sms' do
    expect(interface.receive_sms(room_unoccupied)).to eq "#{room_unoccupied.name} is now available"
  end

  it '#receives an sms when room becomes available' do
    expect(room.exit_room(interface)).to eq "#{room.name} is now available"
  end

  it '#can get team info for an occupied room' do
    office.add_room(room)
    room.enter_room(team)

    expect(interface.check_team(office, room)).to eq "Team The Incredibles in meeting Misplaced Super-Suits"
  end
end

./lib/managerinterface_spec.rb

require 'twilio-ruby'
require 'dotenv/load'
require './lib/roominfointerface'

class ManagerInterface

  def initialize
    @account_sid = ENV['TWILIO_ACCOUNT_SID']
    @auth_token = ENV['auth_token']
    @number = ENV['PHONE_NUMBER']
  end

  def receive_sms(room)
    @client = Twilio::REST::Client.new @account_sid, @auth_token

    message = @client.messages.create(
      body: "#{room.name} is now available",
      to: @number,
      from: '+15005550006',
    )

    message.body
  end

  def check_team(office, team)
    info_interface = RoomInfoInterface.new

    info_interface.get_meeting_info(office, team)
  end
end

./spec/team_spec.rb

require './lib/team'

describe Team do
  let(:team_proton) { Team.new("proton") }

  it '#can set an agenda' do
    expect(team_proton).to respond_to(:make_agenda)
  end

  it '#has attributes name and agenda' do
    team_proton.make_agenda("TypeScript refactor")

    expect(team_proton).to have_attributes(name: 'proton', agenda: "TypeScript refactor")
  end
end

./lib/team.rb

class Team

  attr_reader :name, :agenda

  def initialize(name)
    @name = name
    @agenda = nil
  end

  def make_agenda(agenda)
    @agenda = agenda
  end
end

./spec/roominfointerface_spec.rb

require './lib/office'
require './lib/meetingroom'
require './lib/roominfointerface'

describe RoomInfoInterface do
  let(:info_interface) { RoomInfoInterface.new }
  let(:office) { Office.new }

  let(:room1) { MeetingRoom.new("001") }

  let(:team) { double(name: "Mystery Inc", agenda: "Jinkies!") }

  it '#can access meeting_info' do
    office.add_room(room1)
    room1.enter_room(team)

    expect(info_interface.get_meeting_info(office, room1)).to eq "Team Mystery Inc in meeting Jinkies!"
  end
end

./lib/roominfointerface.rb

require_relative 'office'
require_relative 'meetingroom'

class RoomInfoInterface

  def get_meeting_info(office, room)
    return raise "room doesn't exist" unless office.room_list.include?(room)
    return "room empty" if office.list_available_rooms.include?(room)

    room.meeting_info
  end
end