diff --git a/spec/cb/completion_spec.cr b/spec/cb/completion_spec.cr index 8831656..d12d19c 100644 --- a/spec/cb/completion_spec.cr +++ b/spec/cb/completion_spec.cr @@ -6,7 +6,7 @@ private class CompletionTestClient < CB::Client end def get_teams - [Team.new("def", "my team", false, "manager")] + [Factory.team(**{"id": "def", "name": "my team", "is_personal": false, "role": "manager"})] end def get_firewall_rules(id) @@ -658,34 +658,49 @@ Spectator.describe CB::Completion do # cb team info result = parse("cb team info ") - expect(result).to eq ["def\tmy team"] + expect(result).to have_option "--format" + expect(result).to have_option "--team" + expect(result).to have_option "--no-header" - result = parse("cb team info def ") - expect(result).to eq [] of String + result = parse("cb team info --team def ") + expect(result).to_not have_option "--team" + expect(result).to have_option "--format" + expect(result).to have_option "--no-header" + + result = parse("cb team info --team def --format ") + expect(result).to have_option "list" + expect(result).to have_option "json" + expect(result).to have_option "table" # cb team update result = parse("cb team update ") - expect(result).to eq ["def\tmy team"] + expect(result).to have_option "--billing-email" + expect(result).to have_option "--confirm" + expect(result).to have_option "--enforce-sso" + expect(result).to have_option "--format" + expect(result).to have_option "--name" + expect(result).to have_option "--team" - result = parse("cb team update def ") + result = parse("cb team update --team def ") expect(result).to have_option "--billing-email" expect(result).to have_option "--enforce-sso" expect(result).to have_option "--name" - result = parse("cb team update def --enforce-sso ") + result = parse("cb team update --team def --enforce-sso ") expect(result).to eq ["false", "true"] - result = parse("cb team update def --enforce-sso true ") + result = parse("cb team update --team def --enforce-sso true ") expect(result).to_not have_option "--enforce-sso" expect(result).to have_option "--billing-email" expect(result).to have_option "--name" # cb team destroy result = parse("cb team destroy ") - expect(result).to eq ["def\tmy team"] + expect(result).to have_option "--confirm" + expect(result).to have_option "--team" - result = parse("cb team destroy def ") - expect(result).to eq [] of String + result = parse("cb team destroy --team def ") + expect(result).to have_option "--confirm" end it "completes team-member" do diff --git a/spec/cb/team_spec.cr b/spec/cb/team_spec.cr index 0df4f4c..784c9dc 100644 --- a/spec/cb/team_spec.cr +++ b/spec/cb/team_spec.cr @@ -1,5 +1,74 @@ require "../spec_helper" +Spectator.describe TeamCreate do + subject(action) { described_class.new client: client, output: IO::Memory.new } + + mock_client + + let(team) { Factory.team } + + describe "#validate" do + it "ensures required arguments are presents" do + expect(&.validate).to raise_error Program::Error, /Missing required argument/ + + action.name = team.name + expect(&.validate).to be_true + end + end + + describe "#call" do + before_each { + action.name = team.name + expect(client).to receive(:create_team).and_return team + } + + it "confirms team created (default output)" do + action.call + expect(&.output.to_s).to eq "Created team #{team.id} (#{team.name})\n" + end + + it "outputs json" do + action.format = Format::JSON + action.call + + expected = <<-EXPECTED + { + "id": "l2gnkxjv3beifk6abkraerv7de", + "name": "Test Team", + "is_personal": false, + "role": "admin", + "enforce_sso": false, + "billing_email": "test@example.com" + }\n + EXPECTED + + expect(&.output.to_s).to eq expected + end + + it "raises invalid output format error" do + action.format = Format::Table + expect(&.call).to raise_error Program::Error, /Invalid format: table/ + end + end +end + +Spectator.describe TeamDestroy do + subject(action) { described_class.new client: client, output: IO::Memory.new } + + mock_client + + let(team) { Factory.team } + + describe "#validate" do + it "ensures required arguments are presents" do + expect(&.validate).to raise_error Program::Error, /Missing required argument/ + + action.team_id = team.id + expect(&.validate).to be_true + end + end +end + Spectator.describe TeamInfo do subject(action) { described_class.new client: client, output: IO::Memory.new } @@ -25,5 +94,149 @@ Spectator.describe TeamInfo do expect(&.output.to_s).to eq expected end + + it "outputs as json" do + end + end +end + +Spectator.describe TeamList do + subject(action) { described_class.new client: client, output: IO::Memory.new } + + mock_client + + let(team) { Factory.team } + + describe "#call" do + before_each { + expect(client).to receive(:get_teams).and_return [team] + } + + it "outputs table" do + action.call + + expected = <<-EXPECTED + ID Name Role Billing Email Enforce SSO + l2gnkxjv3beifk6abkraerv7de Test Team Admin test@example.com disabled \n + EXPECTED + + expect(&.output.to_s).to eq expected + end + + it "outputs table without header" do + action.show_header = false + action.call + + expected = <<-EXPECTED + l2gnkxjv3beifk6abkraerv7de Test Team Admin test@example.com disabled \n + EXPECTED + + expect(&.output.to_s).to eq expected + end + + it "outputs json" do + action.format = Format::JSON + action.call + + expected = <<-EXPECTED + { + "teams": [ + { + "id": "l2gnkxjv3beifk6abkraerv7de", + "name": "Test Team", + "is_personal": false, + "role": "admin", + "enforce_sso": false, + "billing_email": "test@example.com" + } + ] + }\n + EXPECTED + + expect(&.output.to_s).to eq expected + end + end +end + +Spectator.describe TeamUpdate do + subject(action) { described_class.new client: client, output: IO::Memory.new } + + mock_client + + let(team) { Factory.team } + + describe "#validate" do + it "ensures required arguments are present" do + expect(&.validate).to raise_error Program::Error, /Missing required argument/ + + action.team_id = team.id + expect(&.validate).to be_true + end + end + + describe "#call" do + before_each { + action.team_id = team.id + action.confirmed = true + action.enforce_sso = true + + expect(client).to receive(:update_team).and_return Factory.team(**{"enforce_sso": true}) + } + + it "outputs list" do + action.call + + expected = <<-EXPECTED + ID: l2gnkxjv3beifk6abkraerv7de + Name: Test Team + Role: Admin + Billing Email: test@example.com + Enforce SSO: enabled \n + EXPECTED + + expect(&.output.to_s).to eq expected + end + + it "outputs table" do + action.format = Format::Table + action.call + + expected = <<-EXPECTED + ID Name Role Billing Email Enforce SSO + l2gnkxjv3beifk6abkraerv7de Test Team Admin test@example.com enabled \n + EXPECTED + + expect(&.output.to_s).to eq expected + end + + it "outputs table without header" do + action.format = Format::Table + action.show_header = false + action.call + + expected = <<-EXPECTED + l2gnkxjv3beifk6abkraerv7de Test Team Admin test@example.com enabled \n + EXPECTED + + expect(&.output.to_s).to eq expected + end + + it "outputs json" do + action.format = Format::JSON + action.call + + expected = <<-EXPECTED + { + "id": "l2gnkxjv3beifk6abkraerv7de", + "name": "Test Team", + "is_personal": false, + "role": "admin", + "enforce_sso": true, + "billing_email": "test@example.com" + }\n + EXPECTED + + expect(&.output.to_s).to eq expected + end end end diff --git a/spec/factory.cr b/spec/factory.cr index 912fa58..3561286 100644 --- a/spec/factory.cr +++ b/spec/factory.cr @@ -121,7 +121,7 @@ module Factory is_personal: false, role: "admin", billing_email: "test@example.com", - enforce_sso: nil, + enforce_sso: false, }.merge(params) CB::Client::Team.new **params diff --git a/src/cb/action.cr b/src/cb/action.cr index 2f2a1c9..554ff26 100644 --- a/src/cb/action.cr +++ b/src/cb/action.cr @@ -114,8 +114,8 @@ module CB # Note: unlike the other macros, this one does not create a nilable boolean, # and instead creates one that defaults to false - macro bool_setter(property) - property {{property}} : Bool = false + macro bool_setter(property, default = false) + property {{property}} : Bool = {{default}} def {{property}}=(str : String) case str.downcase @@ -150,6 +150,10 @@ module CB raise Error.new "Invalid #{field.colorize.bold}: '#{value.to_s.colorize.red}'" end + private def raise_invalid_format(format) + raise Error.new "Invalid format: #{format.to_s.downcase}" + end + private def check_required_args missing = [] of String yield missing diff --git a/src/cb/completion.cr b/src/cb/completion.cr index 48ce6fe..d8c4d4d 100644 --- a/src/cb/completion.cr +++ b/src/cb/completion.cr @@ -675,11 +675,16 @@ class CB::Completion end def team_create + if last_arg? "--format" + return ["json"] + end + if last_arg? "--name" suggest_none end suggest = [] of String + suggest << "--format\tchoose output format" unless has_full_flag? :format suggest << "--name\tteam name" unless has_full_flag? :name suggest end @@ -689,36 +694,70 @@ class CB::Completion end def team_info - return teams if @args.size == 3 - suggest_none + if last_arg? "--format" + return ["json", "list", "table"] + end + + if last_arg? "--team" + suggest_none + end + + suggest = [] of String + suggest << "--format\tchoose output format" unless has_full_flag? :format + suggest << "--no-header" unless has_full_flag? :no_header + suggest << "--team\tteam ID" unless has_full_flag? :team + suggest end def team_update - return teams if @args.size == 3 - if last_arg? "--billing-email" suggest_none end + if last_arg? "--confirm" + suggest_none + end + if last_arg? "--enforce-sso" suggest_bool end + if last_arg? "--format" + ["json", "list", "table"] + end + if last_arg? "--name" suggest_none end + if last_arg? "--team" + suggest_none + end + suggest = [] of String suggest << "--billing-email\tteams billing email address" unless has_full_flag? :billing_email + suggest << "--confirm" unless has_full_flag? :confirm suggest << "--enforce-sso\tenforce SSO access to team" unless has_full_flag? :enforce_sso + suggest << "--format\tchoose output format" unless has_full_flag? :format suggest << "--help\tshow help" unless has_full_flag? :help suggest << "--name\tteam name" unless has_full_flag? :name + suggest << "--team\tteam ID" unless has_full_flag? :team suggest end def team_destroy - return teams if @args.size == 3 - suggest_none + if last_arg? "--team" + suggest_none + end + + if last_arg? "--confirm" + suggest_none + end + + suggest = [] of String + suggest << "--confirm" unless has_full_flag? :confirm + suggest << "--team\tteam ID" unless has_full_flag? :team + suggest end # diff --git a/src/cb/team.cr b/src/cb/team.cr index d41d34b..d94a423 100644 --- a/src/cb/team.cr +++ b/src/cb/team.cr @@ -5,13 +5,53 @@ abstract class CB::TeamAction < CB::APIAction format_setter format - private def output_team_details(t : CB::Client::Team) + bool_setter show_header, true + + private def output_json(team : CB::Client::Team) + output << team.to_pretty_json << '\n' + end + + private def output_json(teams : Array(CB::Client::Team)) + output << { + "teams": teams, + }.to_pretty_json << '\n' + end + + private def output_list(team : CB::Client::Team) table = Table::TableBuilder.new(border: :none) do - row ["ID:", t.id.colorize.t_id] - row ["Name:", t.name.colorize.t_name] - row ["Role:", t.role.to_s.titleize] - row ["Billing Email:", t.billing_email] - row ["Enforce SSO:", (t.enforce_sso.nil? ? "disabled" : t.enforce_sso)] + row ["ID:", team.id.colorize.t_id] + row ["Name:", team.name.colorize.t_name] + row ["Role:", team.role.to_s.titleize] + row ["Billing Email:", team.billing_email] + row ["Enforce SSO:", team.enforce_sso] + end + + output << table.render << '\n' + end + + private def output_table(team : CB::Client::Team) + output_table [team] + end + + private def output_table(teams : Array(CB::Client::Team)) + table = Table::TableBuilder.new(border: :none) do + columns(header: show_header) do + add "ID" + add "Name" + add "Role" + add "Billing Email" + add "Enforce SSO" + end + + teams.each do |team| + row [ + team.id.colorize.t_id, + team.name.colorize.t_name, + team.role.to_s.titleize, + team.billing_email, + team.enforce_sso, + ] + end end output << table.render << '\n' @@ -21,36 +61,61 @@ end class CB::TeamCreate < CB::TeamAction name_setter name - def run + def validate check_required_args do |missing| missing << "name" if name.empty? end + end + + def run + validate team = client.create_team name - output << "Created team #{team}\n" + + case @format + when Format::Default + output << "Created team #{team}\n" + when Format::JSON + output_json team + else + raise_invalid_format @format + end end end class CB::TeamList < CB::TeamAction def run teams = client.get_teams - name_max = teams.map(&.name.size).max? || 0 - teams.each do |team| - output << team.id.colorize.t_id << "\t" - output << team.name.ljust(name_max).colorize.t_name << "\t" - output << team.role.to_s.titleize << "\n" + case @format + when Format::Default, Format::Table + output_table teams + when Format::JSON + output_json teams + else + raise_invalid_format @format end end end class CB::TeamInfo < CB::TeamAction + def validate + end + def run + validate + team = client.get_team team_id case @format when Format::Default, Format::List - output_team_details(team) + output_list team + when Format::JSON + output_json team + when Format::Table + output_json team + else + raise_invalid_format @format end end end @@ -67,11 +132,13 @@ class CB::TeamUpdate < CB::TeamAction bool_setter? enforce_sso bool_setter confirmed - def run + def validate check_required_args do |missing| missing << "team" unless team_id end + end + def run unless confirmed t = client.get_team team_id confirm_action("update", "team", t.name) @@ -83,17 +150,30 @@ class CB::TeamUpdate < CB::TeamAction "name" => name, } - output_team_details(team) + case @format + when Format::Default, Format::List + output_list team + when Format::JSON + output_json team + when Format::Table + output_table team + else + raise_invalid_format @format + end end end class CB::TeamDestroy < CB::TeamAction bool_setter confirmed - def run + def validate check_required_args do |missing| missing << "team" unless team_id end + end + + def run + validate unless confirmed t = client.get_team team_id @@ -101,6 +181,12 @@ class CB::TeamDestroy < CB::TeamAction end team = client.destroy_team team_id - output << "Deleted team #{team}\n" + + case @format + when Format::Default + output << "Deleted team #{team}\n" + else + raise_invalid_format @format + end end end diff --git a/src/cli.cr b/src/cli.cr index ded8060..5f69b34 100755 --- a/src/cli.cr +++ b/src/cli.cr @@ -402,37 +402,101 @@ op = OptionParser.new do |parser| parser.banner = "cb team create <--name>" parser.on("--name NAME", "Team name") { |arg| create.name = arg } + + # Output options. + parser.on("--format FORMAT", "Choose output format.") { |arg| create.format = arg } + + parser.examples = <<-EXAMPLES + Create a new team. + $ cb team create --name + EXAMPLES end parser.on("list", "List available teams.") do - set_action TeamList + list = set_action TeamList parser.banner = "cb team list" + + # Output options. + parser.on("--format FORMAT", "Choose output format. (default: table)") { |arg| list.format = arg } + parser.on("--no-header", "Do not show header with table output.") { |_| list.show_header = false } + + parser.examples = <<-EXAMPLES + List available teams. Output: table + $ cb team list + + List available teams. Output: table without header + $ cb team list --no-header + + List available teams. Output: json + $ cb team list --format=json + EXAMPLES end parser.on("info", "Show a teams details.") do info = set_action TeamInfo - parser.banner = "cb team info " + parser.banner = "cb team info <--team>" + + parser.on("--team ", "Choose team.") { |arg| info.team_id = arg } + + # Output options. + parser.on("--format FORMAT", "Choose output format. (default: list)") { |arg| info.format = arg } + parser.on("--no-header", "Do not show header with table output.") { |_| info.show_header = false } + + parser.examples = <<-EXAMPLES + Get team details. Output: list + $ cb team info --team - positional_args info.team_id + Get team details. Output: table + $ cb team info --team --format=table + + Get team details. Output: table without header + $ cb team info --team --format=table --no-header + + Get team details. Output: json + $ cb team info --team --format=json + EXAMPLES end parser.on("update", "Update a team.") do update = set_action TeamUpdate - parser.banner = "cb team update [options]" + parser.banner = "cb team update <--team> [options]" + parser.on("--team ", "Choose team.") { |arg| update.team_id = arg } parser.on("--billing-email EMAIL", "Team billing email address.") { |arg| update.billing_email = arg } parser.on("--enforce-sso ", "Enforce SSO access to team.") { |arg| update.enforce_sso = arg } parser.on("--name NAME", "Name of the team.") { |arg| update.name = arg } parser.on("--confirm", "Confirm team update.") { |_| update.confirmed = true } - positional_args update.team_id + + # Output options. + parser.on("--format FORMAT", "Choose output format. (default: list)") { |arg| update.format = arg } + parser.on("--no-header", "Do not show header with table output.") { |_| update.show_header = false } + + parser.examples = <<-EXAMPLES + Update team with interactive confirmation. + $ cb team update --team --billing-email + + Update team without interactive confirmation. + $ cb team update --team --billing-email --confirm + + Update team. Output: json + $ cb team update --team --billing-email --format=json + EXAMPLES end parser.on("destroy", "Delete a team.") do destroy = set_action TeamDestroy - parser.banner = "cb team destroy [options]" + parser.banner = "cb team destroy <--team> [options]" + + parser.on("--team", "Choose team.") { |arg| destroy.team_id = arg } + parser.on("--confirm", "Confirm team deletion.") { |_| destroy.confirmed = true } - parser.on("--confirm", "Confirm team deletion.") { destroy.confirmed = true } - positional_args destroy.team_id + parser.examples = <<-EXAMPLES + Destroy team with interactive confirmation. + $ cb team destroy --team + + Destroy team without interactive confirmation. + $ cb team destroy --team --confirm + EXAMPLES end end diff --git a/src/client/team.cr b/src/client/team.cr index 7294f91..58046d4 100644 --- a/src/client/team.cr +++ b/src/client/team.cr @@ -4,17 +4,22 @@ module CB class Client # A team is a small organizational unit in Bridge used to group multiple users # at varying levels of privilege. + jrecord Team, id : String, name : String, is_personal : Bool, role : String?, - billing_email : String? = nil, - enforce_sso : Bool? = nil do + enforce_sso : Bool, + billing_email : String? = nil do def name is_personal ? "personal" : @name end + def enforce_sso + @enforce_sso ? "enabled" : "disabled" + end + def to_s(io : IO) io << id.colorize.t_id << " (" << name.colorize.t_name << ")" end