Skip to content

Commit

Permalink
Merge pull request aeolus-incubator#38 from jistr/output_sorting
Browse files Browse the repository at this point in the history
Output sorting
  • Loading branch information
Petr Blaho committed Jan 29, 2013
2 parents a46ca02 + 2f02dae commit 3d42c0b
Show file tree
Hide file tree
Showing 13 changed files with 170 additions and 18 deletions.
2 changes: 2 additions & 0 deletions features/usage.feature
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Feature: Usage
Options:
[--fields=FIELDS] # Fields (attributes) to print in the listing
[--sort-by=SORT_BY] # Sort output by value of field(s)
[--conductor-url=CONDUCTOR_URL]
[--username=USERNAME]
[--password=PASSWORD]
Expand Down Expand Up @@ -87,6 +88,7 @@ Feature: Usage
Options:
[--fields=FIELDS] # Fields (attributes) to print in the listing
[--sort-by=SORT_BY] # Sort output by value of field(s)
[--conductor-url=CONDUCTOR_URL]
[--username=USERNAME]
[--password=PASSWORD]
Expand Down
27 changes: 26 additions & 1 deletion lib/aeolus_cli/common_cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,20 @@ def banner(task, namespace = nil, subcommand = false)

def method_options_for_resource_list
method_option_fields
# TODO: method_option_sort_by
method_option_sort_by
end

def method_option_fields
method_option :fields,
:type => :string,
:desc => 'Fields (attributes) to print in the listing'
end

def method_option_sort_by
method_option :sort_by,
:type => :string,
:desc => 'Sort output by value of field(s)'
end
end

def load_aeolus_config(options)
Expand Down Expand Up @@ -145,6 +151,25 @@ def resource_fields(fields_option)
fields_option.split(',').map { |option| option.to_sym }
end

def resource_sort_by(sort_by_option)
return nil unless sort_by_option
if sort_by_option == ''
raise Thor::MalformattedArgumentError.new("Option 'sort_by' cannot be empty.")
end
sort_by_option.split(',').map { |option| parse_one_sort_by_option(option) }
end

def parse_one_sort_by_option(option)
case option[-1]
when '+'
[option[0..-2].to_sym, :asc]
when '-'
[option[0..-2].to_sym, :desc]
else
[option.to_sym, :asc]
end
end

def provider_type(type_s)
# we need to hit the API to get the map of provider_type.name =>
# provider_type.id, so make sure we only do this once.
Expand Down
9 changes: 9 additions & 0 deletions lib/aeolus_cli/formatting/format.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'aeolus_cli/formatting/errors'
require 'aeolus_cli/formatting/presenter_sorter'

module AeolusCli::Formatting
# This class (or more precisely, classes that inherit from this one) can
Expand Down Expand Up @@ -30,6 +31,14 @@ def presenter_for(object, fields_override = nil)
presenter_class.new(object, fields_override)
end

# Gets an array of presenters for array of objects. The array sorted
# according to the sort_by parameter. See PresenterSorter for details how
# sorting works.
def presenters_for(objects, fields_override = nil, sort_by = nil)
presenters = objects.map { |object| presenter_for(object, fields_override) }
PresenterSorter.new(presenters, sort_by).sorted_presenters
end

# Registers a presenter to use for objects of some type.
def register(class_name, presenter)
@presenters[class_name] = presenter
Expand Down
9 changes: 5 additions & 4 deletions lib/aeolus_cli/formatting/human_format.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@ def detail(object, fields_override = nil)
def list(objects, fields_override = nil, sort_by = nil)
return if objects.empty?

table = []
presenters = presenters_for(objects, fields_override, sort_by)

table = []
# table header
table << presenter_for(objects.first, fields_override).list_table_header
table << presenters.first.list_table_header

objects.each do |object|
table << presenter_for(object, fields_override).list_item
presenters.each do |presenter|
table << presenter.list_item
end

print_table(table)
Expand Down
6 changes: 4 additions & 2 deletions lib/aeolus_cli/formatting/machine_format.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ def detail(object, fields_override = nil)
def list(objects, fields_override = nil, sort_by = nil)
return if objects.empty?

presenters = presenters_for(objects, fields_override, sort_by)

list = []
objects.each do |object|
list << presenter_for(object, fields_override).list_item
presenters.each do |presenter|
list << presenter.list_item
end

print_list(list, @separator)
Expand Down
36 changes: 36 additions & 0 deletions lib/aeolus_cli/formatting/presenter_sorter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module AeolusCli::Formatting
# Sorts array of Presenter objects by their field values.
class PresenterSorter
# Parameter sort_by is expected like e.g. [[:status, :desc], [:name, :asc]],
# which means "sort by status descending, and if status is the same, sort
# by name ascending".
def initialize(presenters, sort_by)
@presenters = presenters
@sort_by = sort_by
end

def sorted_presenters
return @presenters if @sort_by.nil? || @sort_by.empty?
@presenters.sort do |presenter1, presenter2|
compare(presenter1, presenter2, @sort_by)
end
end

private

def compare(presenter1, presenter2, sort_by)
return 0 if sort_by.empty?
next_sort_by = sort_by.dup
field_name, direction = next_sort_by.shift

current_comparison =
presenter1.send(:field, field_name) <=> presenter2.send(:field, field_name)

if current_comparison == 0
compare(presenter1, presenter2, next_sort_by)
else
direction == :desc ? -current_comparison : current_comparison
end
end
end
end
4 changes: 3 additions & 1 deletion lib/aeolus_cli/provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ class AeolusCli::Provider < AeolusCli::CommonCli
method_options_for_resource_list
def list
providers = AeolusCli::Model::Provider.all
output_format.list(providers, resource_fields(options[:fields]))
output_format.list(providers,
resource_fields(options[:fields]),
resource_sort_by(options[:sort_by]))
end

desc "add PROVIDER_NAME", "Add a provider"
Expand Down
4 changes: 3 additions & 1 deletion lib/aeolus_cli/provider_account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ class AeolusCli::ProviderAccount < AeolusCli::CommonCli
# TODO maybe an optional variable for provider_type
def list
accounts = AeolusCli::Model::ProviderAccount.all_full_detail
output_format.list(accounts, resource_fields(options[:fields]))
output_format.list(accounts,
resource_fields(options[:fields]),
resource_sort_by(options[:sort_by]))
end

desc "add PROVIDER_ACCOUNT_LABEL", "Add a provider account"
Expand Down
20 changes: 20 additions & 0 deletions spec/aeolus_cli/common_cli_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,24 @@
it { should == nil }
end
end

context "#resource_fields" do
context "non-empty fields" do
subject { common_cli.send(:resource_sort_by, "name+,status-,is_cool") }
it { should == [[:name, :asc], [:status, :desc], [:is_cool, :asc]] }
end

context "empty fields" do
subject { common_cli.send(:resource_sort_by, "") }
it do
expect { subject }.to raise_error Thor::MalformattedArgumentError
end
end

context "nil fields" do
subject { common_cli.send(:resource_sort_by, nil) }
it { should == nil }
end
end

end
16 changes: 15 additions & 1 deletion spec/aeolus_cli/formatting/format_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

context "with a presenter" do
let(:presenter) { double('presenter') }
let(:presenter_class) { double('presenter_class') }
let(:presenter_class) { double('presenter_class', :new => presenter) }

before do
format.register("String", presenter_class)
Expand All @@ -32,6 +32,20 @@
subject.should == presenter
end
end

context "#presenters_for" do
let(:sorted_presenters) { double('sorted_presenters') }
let(:sorter) { double('sorter', :sorted_presenters => sorted_presenters) }
before do
AeolusCli::Formatting::PresenterSorter
.should_receive(:new)
.with([presenter, presenter], [:name, :asc])
.and_return(sorter)
end

subject { format.presenters_for(['one', 'two'], nil, [:name, :asc]) }
it { should == sorted_presenters }
end
end

context "printing methods" do
Expand Down
10 changes: 5 additions & 5 deletions spec/aeolus_cli/formatting/human_format_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,17 @@

context "printing a list" do
before do
format.stub_chain("presenter_for.list_item")
.and_return(['list', 'item'])
format.stub_chain("presenter_for.list_table_header")
.and_return(['table', 'header'])
format.stub("presenters_for").and_return([
double('first', :list_item => ['list', 'item'], :list_table_header => ['table', 'header']),
double('second', :list_item => ['list2', 'item2']),
])
end

it "prints the data" do
format.should_receive(:print_table).with([
["table", "header"],
["list", "item"],
["list", "item"],
["list2", "item2"],
])

format.list(['a', 'b'])
Expand Down
12 changes: 9 additions & 3 deletions spec/aeolus_cli/formatting/machine_format_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,18 @@

context "printing a list" do
before do
format.stub_chain("presenter_for.list_item")
.and_return(['list', 'item'])
format.stub("presenters_for").and_return([
double('first', :list_item => ['list', 'item']),
double('second', :list_item => ['list2', 'item2']),
])
end

it "prints the data" do
format.should_receive(:print).with("list;item").twice
format.should_receive(:print_list)
.with([
['list', 'item'],
['list2', 'item2'],
], ';')

format.list(['a', 'b'])
end
Expand Down
33 changes: 33 additions & 0 deletions spec/aeolus_cli/formatting/presenter_sorter_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
require 'aeolus_cli/formatting/presenter_sorter'

describe AeolusCli::Formatting::PresenterSorter do

def stub_fields(presenter, status, owner)
presenter.stub(:field) do |param|
# If I could get local variable by name without using eval, I would :)
# Something like local_variable_get would be handy here.
case param
when :status
status
when :owner
owner
end
end
end

let(:p1) { double('p1').tap { |p| stub_fields(p, 'stopped', 'alice') } }
let(:p2) { double('p2').tap { |p| stub_fields(p, 'stopped', 'joe') } }
let(:p3) { double('p3').tap { |p| stub_fields(p, 'running', 'alice') } }
let(:p4) { double('p4').tap { |p| stub_fields(p, 'running', 'joe') } }

let(:presenters) { [p4, p1, p3, p2] }
let(:sorted_presenters) { [p1, p2, p3, p4] }
let(:sort_by) { [[:status, :desc], [:owner, :asc]] }
let(:sorter) { AeolusCli::Formatting::PresenterSorter.new(presenters, sort_by) }

context "#sorted_data" do
subject { sorter.sorted_presenters }

it { should == sorted_presenters }
end
end

0 comments on commit 3d42c0b

Please sign in to comment.