├── .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 |
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 |
16 |
--------------------------------------------------------------------------------
/delivery/web/views/update_status.erb:
--------------------------------------------------------------------------------
1 |
7 | What is your status?
8 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------