├── .ruby-version ├── test ├── test_helper.rb ├── actions │ ├── remove_status_test.rb │ ├── get_user_test.rb │ ├── get_status_test.rb │ ├── create_user_test.rb │ ├── list_users_test.rb │ ├── update_status_test.rb │ ├── create_status_test.rb │ └── list_statuses_test.rb ├── entities │ ├── user_test.rb │ └── status_test.rb ├── doubles │ ├── user_jack_double.rb │ └── status_jack_double.rb └── contracts │ ├── user_jack_contract_test.rb │ └── status_jack_contract_test.rb ├── app ├── entities │ ├── user.rb │ └── status.rb ├── actions │ ├── remove_status.rb │ ├── list_users.rb │ ├── get_user.rb │ ├── get_status.rb │ ├── list_statuses.rb │ ├── create_user.rb │ ├── create_status.rb │ └── update_status.rb └── contracts │ ├── user_jack_contract.rb │ └── status_jack_contract.rb ├── delivery ├── cli │ ├── data │ │ ├── users.json │ │ └── statuses.json │ └── status.rb └── web │ ├── data │ ├── users.json │ └── statuses.json │ ├── views │ ├── get_user.erb │ ├── sign_up.erb │ ├── get_status.erb │ ├── create_status.erb │ ├── update_status.erb │ ├── remove_status.erb │ ├── index.erb │ └── layout.erb │ └── app.rb ├── Gemfile ├── Rakefile ├── external ├── user_jack.rb ├── status_jack.rb └── fs_plug.rb ├── Gemfile.lock └── README.md /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.1.0 2 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | -------------------------------------------------------------------------------- /app/entities/user.rb: -------------------------------------------------------------------------------- 1 | require 'obvious' 2 | 3 | class User < Obvious::Entity 4 | value :handle, String 5 | value :id, Integer 6 | end 7 | -------------------------------------------------------------------------------- /delivery/cli/data/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "handle": "chef", 4 | "id": 1 5 | }, 6 | { 7 | "handle": "bob", 8 | "id": 2 9 | } 10 | ] -------------------------------------------------------------------------------- /delivery/web/data/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "handle": "chef", 4 | "id": 1 5 | }, 6 | { 7 | "handle": "bob1", 8 | "id": 2 9 | } 10 | ] -------------------------------------------------------------------------------- /delivery/web/views/get_user.erb: -------------------------------------------------------------------------------- 1 |

<%= "User: #{@user[:handle]}" %>

2 | Create Status 3 | -------------------------------------------------------------------------------- /app/entities/status.rb: -------------------------------------------------------------------------------- 1 | require 'obvious' 2 | 3 | class Status < Obvious::Entity 4 | value :text, String 5 | value :user_id, Integer 6 | value :id, Integer 7 | end 8 | -------------------------------------------------------------------------------- /delivery/web/data/statuses.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "text": "doing something awesome", 4 | "user_id": 1, 5 | "id": 2 6 | }, 7 | { 8 | "user_id": 1, 9 | "text": "wat", 10 | "id": 3 11 | } 12 | ] -------------------------------------------------------------------------------- /delivery/web/views/sign_up.erb: -------------------------------------------------------------------------------- 1 |

Sign Up

2 |
3 | 4 | 5 |

6 | 7 |
8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | ruby '~> 3.1.0' 3 | 4 | # To run the tests you should only need obvious and validation 5 | gem 'obvious', '~> 0.2.0' 6 | 7 | # For web app 8 | gem 'sinatra' 9 | gem 'rerun' 10 | gem 'puma' 11 | 12 | # For cli app 13 | gem 'commander' 14 | -------------------------------------------------------------------------------- /test/actions/remove_status_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | require_relative '../../app/actions/remove_status' 3 | require_relative '../doubles/status_jack_double' 4 | 5 | class RemoveStatusTest < Minitest::Test 6 | def test_valid_input 7 | assert(RemoveStatus.new(StatusJackDouble.create :default).exec(id: 1)) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/actions/get_user_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | require_relative '../../app/actions/get_user' 3 | require_relative '../doubles/user_jack_double' 4 | 5 | class GetUserTest < Minitest::Test 6 | def test_valid_input 7 | result = GetUser.new(UserJackDouble.create :default).exec(id: 1) 8 | assert_equal({handle: 'chef', id: 1}, result) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/actions/remove_status.rb: -------------------------------------------------------------------------------- 1 | require 'obvious' 2 | require_relative '../entities/status' 3 | 4 | class RemoveStatus 5 | include Obvious::Obj 6 | 7 | def initialize status_jack 8 | @status_jack = status_jack 9 | end 10 | 11 | define :exec, id: Integer do |input| 12 | # remove the Status object and return the result 13 | @status_jack.remove :id => input[:id] 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /delivery/cli/data/statuses.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "text": "making a sandwich", 4 | "user_id": 1, 5 | "id": 1 6 | }, 7 | { 8 | "text": "doing something awesome", 9 | "user_id": 1, 10 | "id": 2 11 | }, 12 | { 13 | "text": "hello world", 14 | "user_id": 2, 15 | "id": 3 16 | }, 17 | { 18 | "text": "I love the cheese", 19 | "user_id": 1, 20 | "id": 5 21 | } 22 | ] -------------------------------------------------------------------------------- /delivery/web/views/get_status.erb: -------------------------------------------------------------------------------- 1 |

<%= @status[:text] %>

2 | 3 |

4 | - <%= "#{@user[:handle]}" %> 5 |

6 |

7 | 8 | Edit Status 9 |   10 | Remove Status 11 | -------------------------------------------------------------------------------- /test/actions/get_status_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | require_relative '../../app/actions/get_status' 3 | require_relative '../doubles/status_jack_double' 4 | 5 | class GetStatusTest < Minitest::Test 6 | def test_valid_input 7 | result = GetStatus.new(StatusJackDouble.create :default).exec(id: 1) 8 | assert_equal({id: 1, user_id: 1, text: 'making a sandwich'}, result) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /delivery/web/views/create_status.erb: -------------------------------------------------------------------------------- 1 | 7 | 8 |

What is your status?

9 | 10 |
11 | 12 | 13 |
14 | 15 |
16 | -------------------------------------------------------------------------------- /delivery/web/views/update_status.erb: -------------------------------------------------------------------------------- 1 | 7 |

What is your status?

8 |
9 | 10 | 11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /test/actions/create_user_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | require_relative '../../app/actions/create_user' 3 | require_relative '../doubles/user_jack_double' 4 | 5 | class CreateUserTest < Minitest::Test 6 | def test_valid_input 7 | create_user = CreateUser.new(UserJackDouble.create :default) 8 | result = create_user.exec(handle: 'chef') 9 | assert_equal({handle: 'chef', id: 1}, result) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/contracts/user_jack_contract.rb: -------------------------------------------------------------------------------- 1 | require 'obvious' 2 | require_relative '../entities/user' 3 | 4 | class UserJackContract < Obvious::Contract 5 | 6 | contract_for :save, { 7 | input: User.shape, 8 | output: User.shape 9 | } 10 | 11 | contract_for :get, { 12 | input: { id: Integer }, 13 | output: User.shape 14 | } 15 | 16 | contract_for :list, { 17 | input: nil, 18 | output: [ User.shape ] 19 | } 20 | end 21 | -------------------------------------------------------------------------------- /app/actions/list_users.rb: -------------------------------------------------------------------------------- 1 | require_relative '../entities/user' 2 | 3 | class ListUsers 4 | def initialize user_jack 5 | @user_jack = user_jack 6 | end 7 | 8 | def exec 9 | user_data = @user_jack.list 10 | validate user_data 11 | user_data 12 | end 13 | 14 | def validate user_data 15 | # create/populate User objects for validation 16 | user_data.each do |entry| 17 | User.new entry 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | 3 | Rake::TestTask.new(:test) do |t| 4 | t.libs << 'app' 5 | t.libs << 'delivery' 6 | t.libs << 'external' 7 | t.libs << 'test' 8 | # guarantee code coverage (SimpleCov) is loaded first 9 | t.test_files = FileList['test/test_helper.rb'] + 10 | FileList['test/**/*_test.rb'] 11 | end 12 | 13 | task :server do 14 | sh 'cd delivery/web && bundle exec rerun app.rb ' 15 | end 16 | 17 | task default: :test 18 | -------------------------------------------------------------------------------- /delivery/web/views/remove_status.erb: -------------------------------------------------------------------------------- 1 |

Are you sure you want to remove this status?

2 |

3 | <%= @status[:text] %> 4 |

5 |

6 | - <%= @user[:handle] %> 7 |

8 |

9 | 10 |
11 | No 12 |     13 | 14 |
15 | -------------------------------------------------------------------------------- /app/actions/get_user.rb: -------------------------------------------------------------------------------- 1 | require 'obvious' 2 | require_relative '../entities/user' 3 | 4 | class GetUser 5 | include Obvious::Obj 6 | 7 | def initialize user_jack 8 | @user_jack = user_jack 9 | end 10 | 11 | define :exec, id: Integer do |input| 12 | # get the user from the jack 13 | data = @user_jack.get(id: input[:id]) 14 | 15 | # create/populate User object 16 | user = User.new data 17 | 18 | # return the result 19 | user.to_hash 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/actions/list_users_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | require_relative '../../app/actions/list_users' 3 | require_relative '../doubles/user_jack_double' 4 | 5 | class ListUsersTest < Minitest::Test 6 | def test_valid_input 7 | result = ListUsers.new(UserJackDouble.create(:default)).exec 8 | expected_output = [ 9 | { handle: "chef", id: 1 }, 10 | { handle: "ninja", id: 2 } 11 | ] 12 | assert_equal(expected_output, result) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/actions/update_status_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | require_relative '../../app/actions/update_status' 3 | require_relative '../doubles/status_jack_double' 4 | 5 | class UpdateStatusTest < Minitest::Test 6 | def test_valid_input 7 | action = UpdateStatus.new(StatusJackDouble.create :default) 8 | result = action.exec(id: 1, text: 'making a sandwich', user_id: 1) 9 | assert_equal({id: 1, text: 'making a sandwich', user_id: 1}, result) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/actions/create_status_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | require_relative '../../app/actions/create_status' 3 | require_relative '../doubles/status_jack_double' 4 | 5 | class CreateStatusTest < Minitest::Test 6 | def test_valid_input 7 | create_status = CreateStatus.new(StatusJackDouble.create :default) 8 | result = create_status.exec(user_id: 1, text: 'making a sandwich') 9 | assert_equal({user_id: 1, text: 'making a sandwich', id: 1}, result) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /external/user_jack.rb: -------------------------------------------------------------------------------- 1 | require_relative '../app/contracts/user_jack_contract' 2 | require_relative 'fs_plug' 3 | 4 | class UserJack < UserJackContract 5 | 6 | def initialize plug = nil 7 | @plug = FsPlug.new 'data/users.json' 8 | end 9 | 10 | def list 11 | @plug.list 12 | end 13 | 14 | def get input 15 | @plug.get input 16 | end 17 | 18 | def save input 19 | @plug.save input 20 | end 21 | 22 | def remove input 23 | @plug.remove input 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /external/status_jack.rb: -------------------------------------------------------------------------------- 1 | require_relative '../app/contracts/status_jack_contract' 2 | require_relative 'fs_plug' 3 | 4 | class StatusJack < StatusJackContract 5 | def initialize plug = nil 6 | @plug = FsPlug.new 'data/statuses.json' 7 | end 8 | 9 | def list 10 | @plug.list 11 | end 12 | 13 | def get input 14 | @plug.get input 15 | end 16 | 17 | def save input 18 | @plug.save input 19 | end 20 | 21 | def remove input 22 | @plug.remove input 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/actions/get_status.rb: -------------------------------------------------------------------------------- 1 | require 'obvious' 2 | require_relative '../entities/status' 3 | 4 | class GetStatus 5 | include Obvious::Obj 6 | 7 | def initialize status_jack 8 | @status_jack = status_jack 9 | end 10 | 11 | define :exec, id: Integer do |input| 12 | # get the status update from the jack 13 | data = @status_jack.get(id: input[:id]) 14 | 15 | # create/populate Status object 16 | status = Status.new data 17 | 18 | # return the result 19 | status.to_hash 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/actions/list_statuses.rb: -------------------------------------------------------------------------------- 1 | require_relative '../entities/status' 2 | 3 | class ListStatuses 4 | def initialize status_jack 5 | @status_jack = status_jack 6 | end 7 | 8 | def exec 9 | # get the status updates from the jack 10 | status_data = @status_jack.list 11 | validate status_data 12 | status_data 13 | end 14 | 15 | def validate status_data 16 | # create/populate Status objects for validation 17 | status_data.each do |entry| 18 | Status.new(entry) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/contracts/status_jack_contract.rb: -------------------------------------------------------------------------------- 1 | require 'obvious' 2 | require_relative '../entities/status' 3 | 4 | class StatusJackContract < Obvious::Contract 5 | contract_for :save, { 6 | input: Status.shape, 7 | output: Status.shape 8 | } 9 | 10 | contract_for :get, { 11 | input: { id: Integer }, 12 | output: Status.shape 13 | } 14 | 15 | contract_for :list, { 16 | input: nil, 17 | output: [ Status.shape ] 18 | } 19 | 20 | contract_for :remove, { 21 | input: { id: Integer }, 22 | output: true 23 | } 24 | end 25 | -------------------------------------------------------------------------------- /test/actions/list_statuses_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | require_relative '../../app/actions/list_statuses' 3 | require_relative '../doubles/status_jack_double' 4 | 5 | class ListStatusesTest < Minitest::Test 6 | def test_valid_input 7 | result = ListStatuses.new(StatusJackDouble.create(:default)).exec 8 | expected_output = [ 9 | { user_id: 1, text: 'making a sandwich', id: 1 }, 10 | { user_id: 1, text: 'making another sandwich', id: 2 } 11 | ] 12 | assert_equal(expected_output, result) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/actions/create_user.rb: -------------------------------------------------------------------------------- 1 | require 'obvious' 2 | require_relative '../entities/user' 3 | 4 | class CreateUser 5 | include Obvious::Obj 6 | 7 | def initialize user_jack 8 | @user_jack = user_jack 9 | end 10 | 11 | define :exec, handle: String do |input| 12 | # set default id and values for new User entity 13 | input[:id] = -1 14 | 15 | # create/populate User object 16 | user = User.new input 17 | 18 | # save user to jack 19 | result = @user_jack.save user.to_hash 20 | 21 | # return the result 22 | result 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/entities/user_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | require_relative '../../app/entities/user' 3 | 4 | class UserTest < Minitest::Test 5 | def test_valid_input 6 | user = User.new(handle: 'chef', id: 1) 7 | assert_equal('chef', user.handle) 8 | assert_equal(1, user.id) 9 | end 10 | 11 | def test_invalid_input 12 | assert_raises Obvious::TypeError do 13 | User.new(handle: nil, id: nil) 14 | end 15 | end 16 | 17 | def test_existing_shape 18 | assert_equal({handle: String, id: Integer}, User.shape) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /delivery/web/views/index.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

What is your status?

4 | <% @statuses.each do |status| %> 5 |
6 | <%= "#{status[:text]} [link]- #{@users[status[:user_id]][:handle]}" %> 7 |
8 | <% end %> 9 |
10 | 11 |
12 |

Users

13 | <% @users.each do |user| %> 14 | <%= "#{user[1][:handle]}" %> 15 | <% end %> 16 |
17 |
18 | -------------------------------------------------------------------------------- /test/entities/status_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | require_relative '../../app/entities/status' 3 | 4 | class StatusTest < Minitest::Test 5 | def test_valid_input 6 | status = Status.new(id: 1, user_id: 1, text: 'making a sandwich') 7 | assert_equal(1, status.id) 8 | assert_equal(1, status.user_id) 9 | assert_equal('making a sandwich', status.text) 10 | end 11 | 12 | def test_invalid_input 13 | assert_raises Obvious::TypeError do 14 | Status.new(id: nil, user_id: nil, text: nil) 15 | end 16 | end 17 | 18 | def test_existing_shape 19 | assert_equal({text: String, user_id: Integer, id: Integer}, Status.shape) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/actions/create_status.rb: -------------------------------------------------------------------------------- 1 | require 'obvious' 2 | require_relative '../entities/status' 3 | 4 | class CreateStatus 5 | include Obvious::Obj 6 | 7 | def initialize status_jack 8 | @status_jack = status_jack 9 | end 10 | 11 | define :exec, user_id: Integer, text: String do |input| 12 | # set default id and values for new Status entity 13 | input[:id] = -1 # by convention id of -1 will tell the jack save method to create a new row/document/whatever 14 | 15 | # create/populate Status object 16 | status = Status.new input 17 | 18 | # save status to jack 19 | result = @status_jack.save status.to_hash 20 | 21 | # return the result 22 | result 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/actions/update_status.rb: -------------------------------------------------------------------------------- 1 | require 'obvious' 2 | require_relative '../entities/status' 3 | 4 | class UpdateStatus 5 | include Obvious::Obj 6 | 7 | def initialize status_jack 8 | @status_jack = status_jack 9 | end 10 | 11 | define :exec, id: Integer, text: String, user_id: Integer do |input| 12 | # get the status update from the jack 13 | data = @status_jack.get(id: input[:id]) 14 | 15 | # Create valid Status object from data source 16 | status = Status.new data 17 | updated_data = status.to_hash.merge! input 18 | 19 | # update the Status object and save 20 | status = Status.new updated_data 21 | result = @status_jack.save status.to_hash 22 | 23 | # return the result 24 | result 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/doubles/user_jack_double.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../app/contracts/user_jack_contract' 2 | 3 | class UserJackDouble 4 | def self.create behavior 5 | case behavior 6 | when :bad_output 7 | UserJack_BadOutput.new 8 | when :default 9 | UserJack_Default.new 10 | end 11 | end 12 | end 13 | 14 | class UserJack_Default < UserJackContract 15 | def save input 16 | { handle: 'chef', id: 1 } 17 | end 18 | 19 | def get input 20 | { handle: 'chef', id: 1 } 21 | end 22 | 23 | def list 24 | [ 25 | { handle: 'chef', id: 1 }, 26 | { handle: 'ninja', id: 2 } 27 | ] 28 | end 29 | end 30 | 31 | class UserJack_BadOutput < UserJackContract 32 | def save input 33 | nil 34 | end 35 | 36 | def get input 37 | nil 38 | end 39 | 40 | def list 41 | nil 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | commander (4.6.0) 5 | highline (~> 2.0.0) 6 | ffi (1.15.5) 7 | highline (2.0.3) 8 | listen (3.7.1) 9 | rb-fsevent (~> 0.10, >= 0.10.3) 10 | rb-inotify (~> 0.9, >= 0.9.10) 11 | mustermann (1.1.1) 12 | ruby2_keywords (~> 0.0.1) 13 | nio4r (2.5.8) 14 | obvious (0.2.0) 15 | puma (5.6.1) 16 | nio4r (~> 2.0) 17 | rack (2.2.3) 18 | rack-protection (2.1.0) 19 | rack 20 | rb-fsevent (0.11.0) 21 | rb-inotify (0.10.1) 22 | ffi (~> 1.0) 23 | rerun (0.13.1) 24 | listen (~> 3.0) 25 | ruby2_keywords (0.0.5) 26 | sinatra (2.1.0) 27 | mustermann (~> 1.0) 28 | rack (~> 2.2) 29 | rack-protection (= 2.1.0) 30 | tilt (~> 2.0) 31 | tilt (2.0.10) 32 | 33 | PLATFORMS 34 | ruby 35 | x86_64-darwin-19 36 | 37 | DEPENDENCIES 38 | commander 39 | obvious (~> 0.2.0) 40 | puma 41 | rerun 42 | sinatra 43 | 44 | RUBY VERSION 45 | ruby 3.1.0p0 46 | 47 | BUNDLED WITH 48 | 2.3.3 49 | -------------------------------------------------------------------------------- /test/doubles/status_jack_double.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../app/contracts/status_jack_contract' 2 | 3 | class StatusJackDouble 4 | def self.create behavior 5 | case behavior 6 | when :bad_output 7 | StatusJack_BadOutput.new 8 | when :default 9 | StatusJack_Default.new 10 | end 11 | end 12 | end 13 | 14 | class StatusJack_Default < StatusJackContract 15 | def save input 16 | { user_id: 1, text: 'making a sandwich', id: 1 } 17 | end 18 | 19 | def get input 20 | { user_id: 1, text: 'making a sandwich', id: 1 } 21 | end 22 | 23 | def list 24 | [ 25 | { user_id: 1, text: 'making a sandwich', id: 1 }, 26 | { user_id: 1, text: 'making another sandwich', id: 2 } 27 | ] 28 | end 29 | 30 | def remove input 31 | true 32 | end 33 | end 34 | 35 | class StatusJack_BadOutput < StatusJackContract 36 | def save input 37 | nil 38 | end 39 | 40 | def get input 41 | nil 42 | end 43 | 44 | def list 45 | nil 46 | end 47 | 48 | def remove input 49 | nil 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /delivery/web/views/layout.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Status, an Obvious Architecture Example App 8 | 9 | 10 | 11 |
12 |
13 | 14 | Status 15 | 16 |
 
17 | Sign Up 18 |
19 |
20 | <%= yield %> 21 |
22 | 23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Obvious Status 2 | ============== 3 | 4 | Status is an implementation of a twitter style status update app based on the 5 | Obvious Architecture. 6 | 7 | The purpose of this project is be an example app to help explain the concepts of 8 | Obvious. 9 | 10 | Basic Structure 11 | --------------- 12 | 13 | Obvious is not MVC. It's a different approach. The business logic and the data 14 | persistence are separated. 15 | 16 | - `app` folder contains your business rules and entities. 17 | - `external` folder contains your data persistence layer - ORM's, API's, caching, 18 | queues. 19 | - `delivery` folder is where your delivery specific details - UI, 20 | configuration, routing, etc. live. 21 | - `test` folder contains your tests 😃 22 | 23 | Installation 24 | ------------ 25 | 26 | To do basic install run: 27 | 28 | bundle install 29 | 30 | That should install required ruby gems to get the project running. 31 | 32 | Running The Tests 33 | ----------------- 34 | 35 | To run the tests for the app/ is easy. In the project root run: 36 | 37 | rake test 38 | 39 | The tests should all be green and run in about 0.01 seconds. 40 | 41 | Running The Apps 42 | ---------------- 43 | 44 | To run the web app run: 45 | 46 | rake server 47 | 48 | And navigate to http://localhost:4567/ 49 | 50 | To run the cli app run: 51 | 52 | cd delivery/cli 53 | ./status.rb 54 | -------------------------------------------------------------------------------- /test/contracts/user_jack_contract_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | require_relative '../../app/contracts/user_jack_contract' 3 | require_relative '../doubles/user_jack_double' 4 | 5 | class UserJackContractTest < Minitest::Test 6 | def test_save_valid_input 7 | result = UserJackDouble.create(:default).save(id: 1, handle: 'chef') 8 | assert_equal({id: 1, handle: 'chef'}, result) 9 | end 10 | 11 | def test_save_invalid_input 12 | assert_raises Obvious::ContractInputError do 13 | UserJackDouble.create(:default).save({}) 14 | end 15 | end 16 | 17 | def test_save_invalid_output 18 | assert_raises Obvious::ContractOutputError do 19 | UserJackDouble.create(:bad_output).save(id: 1, handle: 'chef') 20 | end 21 | end 22 | 23 | def test_get_valid_input 24 | result = UserJackDouble.create(:default).get(id: 1) 25 | assert_equal({id: 1, handle: 'chef'}, result) 26 | end 27 | 28 | def test_get_invalid_input 29 | assert_raises Obvious::ContractInputError do 30 | UserJackDouble.create(:default).get({}) 31 | end 32 | end 33 | 34 | def test_get_invalid_output 35 | assert_raises Obvious::ContractOutputError do 36 | UserJackDouble.create(:bad_output).get(id: 1) 37 | end 38 | end 39 | 40 | def test_list_valid_input 41 | expected_output = [ 42 | { handle: "chef", id: 1 }, 43 | { handle: "ninja", id: 2 } 44 | ] 45 | assert_equal(expected_output, UserJackDouble.create(:default).list) 46 | end 47 | 48 | def test_list_invalid_output 49 | assert_raises Obvious::ContractOutputError do 50 | UserJackDouble.create(:bad_output).list 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /external/fs_plug.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | class FsPlug 4 | 5 | def initialize filename 6 | @filename = filename 7 | end 8 | 9 | def save input 10 | # open the file 11 | contents = File.read @filename 12 | 13 | data = [] 14 | # parse the json list 15 | query = JSON.parse contents, :symbolize_names => true 16 | 17 | new_element = true if input[:id] == -1 # by convention set new element flag if id == -1 18 | 19 | max_id = -1 20 | # transform the data if needed 21 | query.each do |h| 22 | if input[:id] == h[:id] 23 | h = input 24 | end 25 | max_id = h[:id] if h[:id] > max_id 26 | data << h 27 | end 28 | 29 | # add data to the list if it's a new element 30 | if new_element 31 | input[:id] = max_id + 1 32 | data << input 33 | end 34 | 35 | # save the data back to FS 36 | json_data = JSON.pretty_generate data 37 | File.open(@filename, 'w') {|f| f.write(json_data) } 38 | 39 | # return the transformed data 40 | input 41 | end 42 | 43 | def list 44 | # open the file 45 | contents = File.read @filename 46 | 47 | # parse the json list 48 | data = JSON.parse contents, :symbolize_names => true 49 | 50 | # return the transformed data 51 | data 52 | end 53 | 54 | def get input 55 | # open the file 56 | contents = File.read @filename 57 | 58 | data = [] 59 | # parse the json list 60 | query = JSON.parse contents, :symbolize_names => true 61 | 62 | # transform the data if needed 63 | query.each do |h| 64 | return h if h[:id] == input[:id] 65 | end 66 | 67 | {} 68 | end 69 | 70 | def remove input 71 | # open the file 72 | contents = File.read @filename 73 | 74 | data = [] 75 | # parse the json list 76 | query = JSON.parse contents, :symbolize_names => true 77 | 78 | # transform the data if needed 79 | query.each do |h| 80 | unless h[:id] == input[:id] 81 | data << h 82 | end 83 | end 84 | 85 | # save the data back to FS 86 | json_data = JSON.pretty_generate data 87 | File.open(@filename, 'w') {|f| f.write(json_data) } 88 | 89 | # return true on success 90 | true 91 | end 92 | 93 | end 94 | -------------------------------------------------------------------------------- /test/contracts/status_jack_contract_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | require_relative '../../app/contracts/status_jack_contract' 3 | require_relative '../doubles/status_jack_double' 4 | 5 | class StatusJackContractTest < Minitest::Test 6 | def test_save_valid_input 7 | input = { user_id: 1, text: 'making a sandwich', id: 1 } 8 | result = StatusJackDouble.create(:default).save(input) 9 | assert_equal({user_id: 1, text: 'making a sandwich', id: 1}, result) 10 | end 11 | 12 | def test_save_invalid_input 13 | assert_raises Obvious::ContractInputError do 14 | StatusJackDouble.create(:default).save({}) 15 | end 16 | end 17 | 18 | def test_save_invalid_output 19 | assert_raises Obvious::ContractOutputError do 20 | input = { user_id: 1, text: 'making a sandwich', id: 1 } 21 | StatusJackDouble.create(:bad_output).save(input) 22 | end 23 | end 24 | 25 | def test_get_valid_input 26 | result = StatusJackDouble.create(:default).get(id: 1) 27 | assert_equal({user_id: 1, text: 'making a sandwich', id: 1}, result) 28 | end 29 | 30 | def test_get_invalid_input 31 | assert_raises Obvious::ContractInputError do 32 | StatusJackDouble.create(:default).get({}) 33 | end 34 | end 35 | 36 | def test_get_invalid_output 37 | assert_raises Obvious::ContractOutputError do 38 | StatusJackDouble.create(:bad_output).get(id: 1) 39 | end 40 | end 41 | 42 | def test_list_valid_input 43 | expected_output = [ 44 | { user_id: 1, text: 'making a sandwich', id: 1 }, 45 | { user_id: 1, text: 'making another sandwich', id: 2 } 46 | ] 47 | assert_equal(expected_output, StatusJackDouble.create(:default).list) 48 | end 49 | 50 | def test_list_invalid_output 51 | assert_raises Obvious::ContractOutputError do 52 | StatusJackDouble.create(:bad_output).list 53 | end 54 | end 55 | 56 | def test_remove_valid_input 57 | assert(StatusJackDouble.create(:default).remove(id: 1)) 58 | end 59 | 60 | def test_remove_invalid_input 61 | assert_raises Obvious::ContractInputError do 62 | StatusJackDouble.create(:default).remove({}) 63 | end 64 | end 65 | 66 | def test_remove_invalid_output 67 | assert_raises Obvious::ContractOutputError do 68 | StatusJackDouble.create(:bad_output).remove(id: 1) 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /delivery/web/app.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | 3 | require_relative '../../app/actions/list_statuses' 4 | require_relative '../../app/actions/create_status' 5 | require_relative '../../app/actions/get_status' 6 | require_relative '../../app/actions/update_status' 7 | require_relative '../../app/actions/remove_status' 8 | require_relative '../../app/actions/get_user' 9 | require_relative '../../app/actions/list_users' 10 | require_relative '../../app/actions/create_user' 11 | require_relative '../../external/status_jack' 12 | require_relative '../../external/user_jack' 13 | 14 | get '/' do 15 | list_statuses = ListStatuses.new(StatusJack.new) 16 | @statuses = list_statuses.exec 17 | 18 | list_users = ListUsers.new(UserJack.new) 19 | users = list_users.exec 20 | @users = {} 21 | users.each do |user| 22 | @users[user[:id]] = user 23 | end 24 | 25 | erb :index 26 | end 27 | 28 | get '/:user/create-status' do 29 | erb :create_status 30 | end 31 | 32 | post '/:user/create-status' do 33 | create_status = CreateStatus.new(StatusJack.new) 34 | @status = create_status.exec(user_id: params[:user_id].to_i, text: params[:text]) 35 | redirect '/' 36 | end 37 | 38 | get '/sign-up' do 39 | erb :sign_up 40 | end 41 | 42 | post '/sign-up' do 43 | create_user = CreateUser.new(UserJack.new) 44 | @user = create_user.exec(handle: params[:handle]) 45 | redirect "/user/#{@user[:id]}" 46 | end 47 | 48 | get '/user/:id' do 49 | get_user = GetUser.new(UserJack.new) 50 | @user = get_user.exec(id: params[:id].to_i) 51 | 52 | erb :get_user 53 | end 54 | 55 | get '/status/:id' do 56 | get_status = GetStatus.new(StatusJack.new) 57 | @status = get_status.exec(id: params[:id].to_i) 58 | get_user = GetUser.new(UserJack.new) 59 | @user = get_user.exec(id: @status[:user_id]) 60 | 61 | erb :get_status 62 | end 63 | 64 | get '/status/:id/update' do 65 | get_status = GetStatus.new(StatusJack.new) 66 | @status = get_status.exec(id: params[:id].to_i) 67 | 68 | erb :update_status 69 | end 70 | 71 | post '/status/:id/update' do 72 | update_status = UpdateStatus.new(StatusJack.new) 73 | @status = update_status.exec(id: params[:id].to_i, text: params[:text], user_id: params[:user_id].to_i) 74 | redirect "/status/#{@status[:id]}" 75 | end 76 | 77 | get '/status/:id/remove' do 78 | get_status = GetStatus.new(StatusJack.new) 79 | @status = get_status.exec(id: params[:id].to_i) 80 | get_user = GetUser.new(UserJack.new) 81 | @user = get_user.exec(id: @status[:user_id]) 82 | 83 | erb :remove_status 84 | end 85 | 86 | post '/status/:id/remove' do 87 | remove_status = RemoveStatus.new(StatusJack.new) 88 | result = remove_status.exec(id: params[:id].to_i) 89 | if result == true 90 | redirect '/' 91 | else 92 | 'ERROR' 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /delivery/cli/status.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'commander/import' 3 | 4 | require_relative '../../app/actions/list_statuses' 5 | require_relative '../../app/actions/create_status' 6 | require_relative '../../app/actions/get_status' 7 | require_relative '../../app/actions/update_status' 8 | require_relative '../../app/actions/remove_status' 9 | require_relative '../../app/actions/get_user' 10 | require_relative '../../app/actions/list_users' 11 | require_relative '../../app/actions/create_user' 12 | 13 | require_relative '../../external/status_jack' 14 | require_relative '../../external/user_jack' 15 | 16 | program :name, 'Status' 17 | program :version, '1.0' 18 | program :description, 'This is a cli version of Status' 19 | 20 | command :list do |c| 21 | c.action do |args, options| 22 | entity = args[0] 23 | if entity == 'users' 24 | list_users = ListUsers.new(UserJack.new) 25 | users = list_users.exec 26 | puts 'Users:' 27 | users.each do |user| 28 | puts "#{user[:handle]} - id: #{user[:id]}" 29 | end 30 | elsif entity == 'statuses' 31 | list_statuses = ListStatuses.new(StatusJack.new) 32 | statuses = list_statuses.exec 33 | puts 'Statuses:' 34 | statuses.each do |status| 35 | puts "#{status[:text]} - status id: #{status[:id]} user id: #{status[:user_id]}" 36 | end 37 | end 38 | end 39 | end 40 | 41 | command :get do |c| 42 | c.action do |args, options| 43 | entity = args[0] 44 | id = args[1] 45 | if entity == 'user' 46 | get_user = GetUser.new(UserJack.new) 47 | user = get_user.exec(id: id.to_i) 48 | puts 'User:' 49 | puts "#{user[:handle]} - id: #{user[:id]}" 50 | elsif entity == 'status' 51 | get_status = GetStatus.new(StatusJack.new) 52 | status = get_status.exec(id: id.to_i) 53 | puts 'Status:' 54 | puts "#{status[:text]} - status id: #{status[:id]} user id: #{status[:user_id]}" 55 | end 56 | end 57 | end 58 | 59 | command :create do |c| 60 | c.action do |args, options| 61 | entity = args[0] 62 | if entity == 'user' 63 | create_user = CreateUser.new UserJack.new 64 | user = create_user.execute with_user_handle: args[1] 65 | puts 'New User:' 66 | puts "#{user[:handle]} - id: #{user[:id]}" 67 | elsif entity == 'status' 68 | create_status = CreateStatus.new StatusJack.new 69 | status = create_status.execute with_user_id: args[1].to_i, and_text: args[2] 70 | puts 'Status:' 71 | puts "#{status[:text]} - status id: #{status[:id]} user id: #{status[:user_id]}" 72 | end 73 | end 74 | end 75 | 76 | command :update do |c| 77 | c.action do |args, options| 78 | entity = args[0] 79 | if entity == 'user' 80 | # we don't update users now, but we might later 81 | elsif entity == 'status' 82 | update_status = UpdateStatus.new StatusJack.new 83 | status = update_status.execute for_status_id: args[1].to_i, with_text: args[3], and_user_id: args[2].to_i 84 | puts 'Status:' 85 | puts "#{status[:text]} - status id: #{status[:id]} user id: #{status[:user_id]}" 86 | end 87 | end 88 | end 89 | 90 | 91 | command :remove do |c| 92 | c.action do |args, options| 93 | entity = args[0] 94 | if entity == 'user' 95 | # we don't update users now, but we might later 96 | elsif entity == 'status' 97 | remove_status = RemoveStatus.new StatusJack.new 98 | status = remove_status.execute where_id: args[1].to_i 99 | puts 'Status Removed' 100 | end 101 | end 102 | end 103 | --------------------------------------------------------------------------------