├── .rspec ├── NOTES.md ├── Rakefile ├── config └── environment.rb ├── lib ├── author.rb ├── category.rb └── story.rb └── spec ├── author_spec.rb ├── category_spec.rb ├── spec_helper.rb └── story_spec.rb /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /NOTES.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learn-co-curriculum/collaborating-objects-tdd-sample/d30de351fec6412492c4355a655f8037a65e6453/NOTES.md -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require_relative './config/environment' 2 | 3 | puts "Welcome to Authors, Stories, and Categories..." 4 | 5 | def reload! 6 | load './lib/author.rb' 7 | load './lib/category.rb' 8 | load './lib/story.rb' 9 | end 10 | 11 | desc "A console" 12 | task :console do 13 | Pry.start 14 | end 15 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | require 'pry' 2 | 3 | require_relative '../lib/author' 4 | require_relative '../lib/category' 5 | require_relative '../lib/story' 6 | 7 | class AssociationTypeMismatchError < TypeError; end 8 | -------------------------------------------------------------------------------- /lib/author.rb: -------------------------------------------------------------------------------- 1 | class Author 2 | attr_accessor :name 3 | 4 | def initialize 5 | @stories = [] # has_many stories interface 6 | end 7 | 8 | def stories # has_many stories interface 9 | @stories.dup.freeze 10 | end 11 | 12 | def add_story(story) # has_many stories interface 13 | raise AssociationTypeMismatchError, "#{story.class} received, Story expected." if !story.is_a?(Story) 14 | @stories << story 15 | story.author = self unless story.author == self 16 | end 17 | 18 | def bibliography 19 | self.stories.collect{|s| s.name} # You need all stories to be instances of Story because they must respond to #name to work 20 | # @stories.collect(&:name) #=> Symbol to Proc 21 | end 22 | 23 | def categories # has_many categories through stories 24 | self.stories.collect{|s| s.category}.uniq 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/category.rb: -------------------------------------------------------------------------------- 1 | class Category 2 | attr_accessor :name 3 | 4 | def initialize 5 | @stories = [] # has_many stories interface 6 | end 7 | 8 | def stories # has_many stories interface 9 | @stories.dup.freeze 10 | end 11 | 12 | def add_story(story) # has_many stories interface 13 | raise AssociationTypeMismatchError, "#{story.class} received, Story expected." if !story.is_a?(Story) 14 | @stories << story 15 | story.category = self unless story.category == self 16 | end 17 | 18 | def authors # has_many authors through stories 19 | self.stories.collect{|s| s.author}.uniq 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/story.rb: -------------------------------------------------------------------------------- 1 | class Story 2 | attr_accessor :name 3 | attr_reader :author # Belongs to author 4 | 5 | def author=(author) # Belongs to author 6 | raise AssociationTypeMismatchError, "#{author.class} received, Author expected." if !author.is_a?(Author) 7 | @author = author 8 | author.add_story(self) unless author.stories.include?(self) 9 | end 10 | 11 | attr_reader :category # Belongs to category 12 | 13 | def category=(category) # Belongs to category 14 | raise AssociationTypeMismatchError, "#{category.class} received, Category expected." if !category.is_a?(Category) 15 | @category = category 16 | category.add_story(self) unless category.stories.include?(self) 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /spec/author_spec.rb: -------------------------------------------------------------------------------- 1 | describe Author do 2 | let(:author){Author.new.tap{|a| a.name = "Ernest Hemingway"}} 3 | let(:story){Story.new.tap{|s| s.name = "The Old Man and the Sea"}} 4 | 5 | it 'has a name' do 6 | expect(author.name).to eq("Ernest Hemingway") 7 | end 8 | 9 | context 'has many stories' do 10 | describe '#stories' do 11 | it 'has an empty array of stories when initialized' do 12 | expect(author.stories).to match_array([]) 13 | end 14 | 15 | it 'returns a frozen copy of the stories array' do 16 | expect(author.stories).to be_frozen 17 | end 18 | end 19 | 20 | describe '#add_story' do 21 | it 'can add a story instance onto it' do 22 | author.add_story(story) 23 | 24 | expect(author.stories).to include(story) 25 | end 26 | 27 | it "reciprocates assigning this author as the story's author" do 28 | author.add_story(story) 29 | 30 | expect(story.author).to eq(author) 31 | end 32 | 33 | it 'only allows stories to be pushed onto it' do 34 | story = "Old Man and the Sea" 35 | 36 | expect{author.add_story(story)}.to raise_error(AssociationTypeMismatchError) 37 | end 38 | end 39 | 40 | describe '#bibliography' do 41 | it 'returns an array of all the stories names of an author' do 42 | author = Author.new 43 | 44 | story_1 = Story.new.tap{|s| s.name = "Old Man and the Sea"} 45 | story_2 = Story.new.tap{|s| s.name = "The Sun Also Rises"} 46 | story_3 = Story.new.tap{|s| s.name = "For Whom the Bell Tolls"} 47 | 48 | author.add_story(story_1) 49 | author.add_story(story_2) 50 | author.add_story(story_3) 51 | 52 | expect(author.bibliography).to match_array(["Old Man and the Sea", "The Sun Also Rises", "For Whom the Bell Tolls"]) 53 | end 54 | end 55 | end 56 | 57 | context 'has many categories through stories' do 58 | it 'returns the collection of unique category instances based on the stories' do 59 | fiction = Category.new{|c| c.name = "Fiction"} 60 | non_fiction = Category.new{|c| c.name = "Non Fiction"} 61 | 62 | story_1 = Story.new.tap{|s| s.name = "Old Man and the Sea"; s.category = fiction} 63 | story_2 = Story.new.tap{|s| s.name = "The Sun Also Rises"; s.category = fiction} 64 | story_3 = Story.new.tap{|s| s.name = "A Moveable Feast"; s.category = non_fiction} 65 | 66 | author.add_story(story_1) 67 | author.add_story(story_2) 68 | author.add_story(story_3) 69 | 70 | expect(author.categories).to match_array([fiction, non_fiction]) 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/category_spec.rb: -------------------------------------------------------------------------------- 1 | describe Category do 2 | let(:category){Category.new.tap{|c| c.name = "Fiction"}} 3 | let(:story){Story.new.tap{|s| s.name = "The Old Man and the Sea"}} 4 | 5 | it 'has a name' do 6 | # Expectation 7 | expect(category.name).to eq("Fiction") 8 | end 9 | 10 | context 'has many stories' do 11 | describe '#stories' do 12 | it 'has an empty array of stories when initialized' do 13 | expect(category.stories).to match_array([]) 14 | end 15 | 16 | it 'returns a frozen copy of the stories array' do 17 | expect(category.stories).to be_frozen 18 | end 19 | end 20 | 21 | describe '#add_story' do 22 | it 'can add a story instance onto it' do 23 | category.add_story(story) 24 | 25 | expect(category.stories).to include(story) 26 | end 27 | 28 | it "reciprocates assigning this category as the story's category" do 29 | category.add_story(story) 30 | 31 | expect(story.category).to eq(category) 32 | end 33 | 34 | it 'only allows stories to be pushed onto it' do 35 | story = "Old Man and the Sea" 36 | 37 | expect{category.add_story(story)}.to raise_error(AssociationTypeMismatchError) 38 | end 39 | end 40 | end 41 | 42 | context 'has many authors through stories' do 43 | it 'returns the collection of unique category instances based on the stories' do 44 | king = Author.new{|c| c.name = "Steven King"} 45 | patterson = Author.new{|c| c.name = "John Patterson"} 46 | 47 | story_1 = Story.new.tap{|s| s.name = "That Scary One"; s.author = patterson} 48 | story_2 = Story.new.tap{|s| s.name = "That Mystery One"; s.author = patterson} 49 | story_3 = Story.new.tap{|s| s.name = "The Shining"; s.author = king} 50 | 51 | story_1.category = category 52 | story_2.category = category 53 | story_3.category = category 54 | 55 | expect(category.authors).to match_array([king, patterson]) 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require_relative '../config/environment' 2 | 3 | # This file was generated by the `rspec --init` command. Conventionally, all 4 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 5 | # The generated `.rspec` file contains `--require spec_helper` which will cause 6 | # this file to always be loaded, without a need to explicitly require it in any 7 | # files. 8 | # 9 | # Given that it is always loaded, you are encouraged to keep this file as 10 | # light-weight as possible. Requiring heavyweight dependencies from this file 11 | # will add to the boot time of your test suite on EVERY test run, even for an 12 | # individual file that may not need all of that loaded. Instead, consider making 13 | # a separate helper file that requires the additional dependencies and performs 14 | # the additional setup, and require it from the spec files that actually need 15 | # it. 16 | # 17 | # The `.rspec` file also contains a few flags that are not defaults but that 18 | # users commonly want. 19 | # 20 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 21 | RSpec.configure do |config| 22 | # rspec-expectations config goes here. You can use an alternate 23 | # assertion/expectation library such as wrong or the stdlib/minitest 24 | # assertions if you prefer. 25 | config.expect_with :rspec do |expectations| 26 | # This option will default to `true` in RSpec 4. It makes the `description` 27 | # and `failure_message` of custom matchers include text for helper methods 28 | # defined using `chain`, e.g.: 29 | # be_bigger_than(2).and_smaller_than(4).description 30 | # # => "be bigger than 2 and smaller than 4" 31 | # ...rather than: 32 | # # => "be bigger than 2" 33 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 34 | end 35 | 36 | # rspec-mocks config goes here. You can use an alternate test double 37 | # library (such as bogus or mocha) by changing the `mock_with` option here. 38 | config.mock_with :rspec do |mocks| 39 | # Prevents you from mocking or stubbing a method that does not exist on 40 | # a real object. This is generally recommended, and will default to 41 | # `true` in RSpec 4. 42 | mocks.verify_partial_doubles = true 43 | end 44 | 45 | # The settings below are suggested to provide a good initial experience 46 | # with RSpec, but feel free to customize to your heart's content. 47 | =begin 48 | # These two settings work together to allow you to limit a spec run 49 | # to individual examples or groups you care about by tagging them with 50 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 51 | # get run. 52 | config.filter_run :focus 53 | config.run_all_when_everything_filtered = true 54 | 55 | # Allows RSpec to persist some state between runs in order to support 56 | # the `--only-failures` and `--next-failure` CLI options. We recommend 57 | # you configure your source control system to ignore this file. 58 | config.example_status_persistence_file_path = "spec/examples.txt" 59 | 60 | # Limits the available syntax to the non-monkey patched syntax that is 61 | # recommended. For more details, see: 62 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 63 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 64 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 65 | config.disable_monkey_patching! 66 | 67 | # This setting enables warnings. It's recommended, but in some cases may 68 | # be too noisy due to issues in dependencies. 69 | config.warnings = true 70 | 71 | # Many RSpec users commonly either run the entire suite or an individual 72 | # file, and it's useful to allow more verbose output when running an 73 | # individual spec file. 74 | if config.files_to_run.one? 75 | # Use the documentation formatter for detailed output, 76 | # unless a formatter has already been configured 77 | # (e.g. via a command-line flag). 78 | config.default_formatter = 'doc' 79 | end 80 | 81 | # Print the 10 slowest examples and example groups at the 82 | # end of the spec run, to help surface which specs are running 83 | # particularly slow. 84 | config.profile_examples = 10 85 | 86 | # Run specs in random order to surface order dependencies. If you find an 87 | # order dependency and want to debug it, you can fix the order by providing 88 | # the seed, which is printed after each run. 89 | # --seed 1234 90 | config.order = :random 91 | 92 | # Seed global randomization in this process using the `--seed` CLI option. 93 | # Setting this allows you to use `--seed` to deterministically reproduce 94 | # test failures related to randomization by passing the same `--seed` value 95 | # as the one that triggered the failure. 96 | Kernel.srand config.seed 97 | =end 98 | end 99 | -------------------------------------------------------------------------------- /spec/story_spec.rb: -------------------------------------------------------------------------------- 1 | describe Story do 2 | let(:story){Story.new.tap{|s| s.name = "The Old Man and the Sea"}} 3 | let(:author){Author.new.tap{|a| a.name = "Ernest Hemingway"}} 4 | let(:category){Category.new.tap{|c| c.name = "Fiction"}} 5 | 6 | it 'has a name' do 7 | expect(story.name).to eq("The Old Man and the Sea") 8 | end 9 | 10 | context 'with an author' do 11 | describe '#author=' do 12 | it 'can set an author' do 13 | story.author = author 14 | expect(story.author).to eq(author) 15 | end 16 | 17 | it 'throws an error if you assign anything besides an instace of Author' do 18 | expect{story.author = "Ernest Hemingway"}.to raise_error(AssociationTypeMismatchError) 19 | end 20 | 21 | it 'reciprocates the story into the authors stories collection' do 22 | story.author = author 23 | 24 | expect(author.stories).to include(story) 25 | end 26 | end 27 | end 28 | 29 | context 'with a category' do 30 | describe '#category=' do 31 | it 'can set an category' do 32 | story.category = category 33 | expect(story.category).to eq(category) 34 | end 35 | 36 | it 'throws an error if you assign anything besides an instace of Author' do 37 | expect{story.category = "Ernest Hemingway"}.to raise_error(AssociationTypeMismatchError) 38 | end 39 | 40 | it 'reciprocates the story into the categorys stories collection' do 41 | story.category = category 42 | 43 | expect(category.stories).to include(story) 44 | end 45 | end 46 | end 47 | end 48 | --------------------------------------------------------------------------------