diff --git a/lib/probe_dock_ruby/project.rb b/lib/probe_dock_ruby/project.rb deleted file mode 100644 index dfa78ac..0000000 --- a/lib/probe_dock_ruby/project.rb +++ /dev/null @@ -1,46 +0,0 @@ -module ProbeDockProbe - class Project - attr_accessor :api_id, :version, :category, :tags, :tickets - - def initialize options = {} - update({ - tags: [], - tickets: [] - }.merge(options)) - end - - def empty? - %i(api_id version category tags tickets).all?{ |attr| send(attr).nil? || send(attr).respond_to?(:empty?) && send(attr).empty? } - end - - def update options = {} - %i(version api_id category).each{ |attr| set_string(attr, options[attr]) if options.key?(attr) } - %i(tags tickets).each{ |attr| set_array(attr, options[attr]) if options.key?(attr) } - end - - def clear - %i(api_id version category).each{ |attr| set_string(attr, nil) } - %i(tags tickets).each{ |attr| set_array(attr, []) } - end - - def validate! - required = { "version" => @version, "API identifier" => @api_id } - missing = required.inject([]){ |memo,(k,v)| v.to_s.strip.length <= 0 ? memo << k : memo } - raise PayloadError.new("Missing project options: #{missing.join ', '}") if missing.any? - end - - private - - def set_string attr, value - instance_variable_set "@#{attr}", value ? value.to_s : nil - end - - def set_array attr, value - instance_variable_set "@#{attr}", wrap(value).compact - end - - def wrap a - a.kind_of?(Array) ? a : [ a ] - end - end -end diff --git a/lib/probe_dock_ruby/scm.rb b/lib/probe_dock_ruby/scm.rb deleted file mode 100644 index 2d2848b..0000000 --- a/lib/probe_dock_ruby/scm.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'ostruct' - -module ProbeDockProbe - class Scm - attr_accessor :name, :version, :dirty, :remote - - def initialize - clear - end - - def update options = {} - %i(name version).each do |key| - instance_variable_set("@#{key}", options[key] ? options[key].to_s : nil) if options.key?(key) - end - - @dirty = !!options[:dirty] if options.key?(:dirty) - - remote_options = options[:remote].kind_of?(Hash) ? options[:remote] : {} - @remote[:name] = remote_options[:name] ? remote_options[:name].to_s : nil if remote_options.key?(:name) - @remote[:ahead] = remote_options[:ahead] ? remote_options[:ahead].to_i : nil if remote_options.key?(:ahead) - @remote[:behind] = remote_options[:behind] ? remote_options[:behind].to_i : nil if remote_options.key?(:behind) - - url = @remote[:url] - remote_url_options = remote_options[:url].kind_of?(Hash) ? remote_options[:url] : {} - url[:fetch] = remote_url_options[:fetch] ? remote_url_options[:fetch].to_s : nil if remote_url_options.key?(:fetch) - url[:push] = remote_url_options[:push] ? remote_url_options[:push].to_s : nil if remote_url_options.key?(:push) - end - - def clear - %i(name version dirty).each{ |attr| instance_variable_set("@#{attr}", nil) } - @remote = OpenStruct.new({ - url: OpenStruct.new - }) - end - end -end diff --git a/lib/probedock-ruby.rb b/lib/probedock-ruby.rb index a6bf7be..c0c8a65 100644 --- a/lib/probedock-ruby.rb +++ b/lib/probedock-ruby.rb @@ -1 +1 @@ -require 'probe_dock_ruby' +require 'probedock_ruby' diff --git a/lib/probe_dock_ruby.rb b/lib/probedock_ruby.rb similarity index 56% rename from lib/probe_dock_ruby.rb rename to lib/probedock_ruby.rb index fccc837..94b7f05 100644 --- a/lib/probe_dock_ruby.rb +++ b/lib/probedock_ruby.rb @@ -6,4 +6,4 @@ class Error < StandardError; end class PayloadError < Error; end end -Dir[File.join File.dirname(__FILE__), File.basename(__FILE__, '.*'), '*.rb'].each{ |lib| require lib } +Dir[File.join(File.dirname(__FILE__), File.basename(__FILE__, '.*'), '*.rb')].each{ |lib| require lib } diff --git a/lib/probe_dock_ruby/annotation.rb b/lib/probedock_ruby/annotation.rb similarity index 100% rename from lib/probe_dock_ruby/annotation.rb rename to lib/probedock_ruby/annotation.rb diff --git a/lib/probe_dock_ruby/client.rb b/lib/probedock_ruby/client.rb similarity index 100% rename from lib/probe_dock_ruby/client.rb rename to lib/probedock_ruby/client.rb diff --git a/lib/probe_dock_ruby/config.rb b/lib/probedock_ruby/config.rb similarity index 97% rename from lib/probe_dock_ruby/config.rb rename to lib/probedock_ruby/config.rb index b59c75c..c2460d3 100644 --- a/lib/probe_dock_ruby/config.rb +++ b/lib/probedock_ruby/config.rb @@ -1,14 +1,24 @@ require 'yaml' +require File.join(File.dirname(__FILE__), 'configurable.rb') module ProbeDockProbe class Config + include Configurable + class Error < ProbeDockProbe::Error; end + configurable({ + publish: :boolean, + local_mode: :boolean, + print_payload: :boolean, + save_payload: :boolean + }) + # TODO: add silent/verbose option(s) - attr_writer :publish, :local_mode, :print_payload, :save_payload attr_reader :project, :server, :scm, :workspace, :load_warnings - def initialize + def initialize options = {} + super options initialize_servers @project = Project.new @scm = Scm.new @@ -64,14 +74,14 @@ def load! &block def apply_configuration! config - @publish = config.fetch(:publish, true) - @server_name = config[:server] + @publish = !!config.fetch(:publish, true) @local_mode = !!config[:local] self.workspace = config[:workspace] @print_payload = !!config[:payload][:print] @save_payload = !!config[:payload][:save] + @server_name = config[:server] build_servers! config project_options = config[:project] @@ -83,7 +93,6 @@ def apply_configuration! config @scm.update(config[:scm]) - # TODO: test config block yield self if block_given? end diff --git a/lib/probedock_ruby/configurable.rb b/lib/probedock_ruby/configurable.rb new file mode 100644 index 0000000..41e5f9d --- /dev/null +++ b/lib/probedock_ruby/configurable.rb @@ -0,0 +1,132 @@ +module ProbeDockProbe + module Configurable + def self.included mod + mod.extend ClassMethods + end + + def initialize attrs = {} + set_configurable_attrs(nil_attrs.merge(attrs.kind_of?(Hash) ? attrs : {})) + end + + def empty? + self.class.configurable_attrs.all? do |attr| + value = send(attr) + value.nil? || value.respond_to?(:empty?) && value.empty? + end + end + + def update attrs = {} + set_configurable_attrs(attrs) + end + + def clear + self.class.configurable_attrs.each do |attr| + value = send(attr) + if value.kind_of?(Configurable) + value.clear + else + send("#{attr}=", nil) + end + end + end + + def to_h + self.class.configurable_attrs.inject({}) do |memo,attr| + + value = send(attr) + if value.kind_of?(Configurable) + memo[attr] = value.to_h + elsif !attr_empty?(attr) + memo[attr] = value + end + + memo + end + end + + private + + def attr_empty?(attr) + value = send(attr) + value.nil? || value.respond_to?(:empty?) && value.empty? + end + + def nil_attrs + self.class.configurable_attrs.inject({}) do |memo,attr| + memo[attr] = nil + memo + end + end + + def set_boolean attr, value + instance_variable_set("@#{attr}", value.nil? ? nil : !!value) + end + + def set_integer attr, value + instance_variable_set("@#{attr}", value.nil? ? nil : value.to_s.to_i) + end + + def set_string attr, value + instance_variable_set("@#{attr}", value.nil? ? nil : value.to_s) + end + + def set_string_array attr, value + instance_variable_set("@#{attr}", wrap(value).compact.collect(&:to_s)) + end + + def set_configurable klass, attr, value + variable = "@#{attr}" + if configurable = instance_variable_get(variable) + configurable.update value + else + instance_variable_set("@#{attr}", klass.new(value)) + end + end + + def set_configurable_attrs attrs = {} + return self unless attrs.kind_of?(Hash) + + self.class.configurable_attrs.each do |attr| + send("#{attr}=", attrs[attr]) if attrs.key?(attr) + end + + self + end + + def wrap a + a.kind_of?(Array) ? a : [ a ] + end + + module ClassMethods + def configurable attr_definitions = {} + + @configurable_attrs = attr_definitions.keys + + attr_definitions.each do |attr,type| + + setter = if type.kind_of?(Class) && type.included_modules.include?(Configurable) + :configurable + elsif type.kind_of?(Symbol) + type + else + raise "Unsupported type of configurable attribute #{type.inspect}; must be either a symbol or a configurable class" + end + + attr_reader attr + + define_method "#{attr}=" do |value| + if setter == :configurable + send :set_configurable, type, attr, value + else + send "set_#{type}", attr, value + end + end + end + end + + def configurable_attrs *attrs + @configurable_attrs || [] + end + end + end +end diff --git a/lib/probedock_ruby/project.rb b/lib/probedock_ruby/project.rb new file mode 100644 index 0000000..c1d3724 --- /dev/null +++ b/lib/probedock_ruby/project.rb @@ -0,0 +1,21 @@ +require File.join(File.dirname(__FILE__), 'configurable.rb') + +module ProbeDockProbe + class Project + include Configurable + + configurable({ + api_id: :string, + version: :string, + category: :string, + tags: :string_array, + tickets: :string_array + }) + + def validate! + required = { "version" => @version, "API identifier" => @api_id } + missing = required.inject([]){ |memo,(k,v)| v.to_s.strip.length <= 0 ? memo << k : memo } + raise PayloadError.new("Missing project options: #{missing.join ', '}") if missing.any? + end + end +end diff --git a/lib/probedock_ruby/scm.rb b/lib/probedock_ruby/scm.rb new file mode 100644 index 0000000..88de765 --- /dev/null +++ b/lib/probedock_ruby/scm.rb @@ -0,0 +1,35 @@ +require 'ostruct' +require File.join(File.dirname(__FILE__), 'configurable.rb') + +module ProbeDockProbe + class ScmRemoteUrl + include Configurable + + configurable({ + fetch: :string, + push: :string + }) + end + + class ScmRemote + include Configurable + + configurable({ + name: :string, + ahead: :integer, + behind: :integer, + url: ScmRemoteUrl + }) + end + + class Scm + include Configurable + + configurable({ + name: :string, + version: :string, + dirty: :boolean, + remote: ScmRemote + }) + end +end diff --git a/lib/probe_dock_ruby/server.rb b/lib/probedock_ruby/server.rb similarity index 71% rename from lib/probe_dock_ruby/server.rb rename to lib/probedock_ruby/server.rb index 067a06e..efaa944 100644 --- a/lib/probe_dock_ruby/server.rb +++ b/lib/probedock_ruby/server.rb @@ -1,10 +1,10 @@ require 'oj' require 'httparty' +require File.join(File.dirname(__FILE__), 'configurable.rb') module ProbeDockProbe class Server - attr_reader :name - attr_accessor :api_url, :api_token, :project_api_id + include Configurable class Error < ProbeDockProbe::Error attr_reader :response @@ -15,22 +15,17 @@ def initialize msg, response = nil end end - def initialize options = {} - @name = options[:name].to_s.strip if options[:name] - @api_url = options[:api_url].to_s if options[:api_url] - @api_token = options[:api_token].to_s if options[:api_token] - @project_api_id = options[:project_api_id].to_s if options[:project_api_id] - end + configurable({ + api_url: :string, + api_token: :string, + project_api_id: :string + }) - def clear - @name = nil - @api_url = nil - @api_token = nil - @project_api_id = nil - end + attr_reader :name - def empty? - %i(api_url api_token project_api_id).all?{ |attr| send(attr).nil? || send(attr).empty? } + def initialize options = {} + super options + @name = options[:name].to_s.strip if options[:name] end def upload payload diff --git a/lib/probe_dock_ruby/tasks.rb b/lib/probedock_ruby/tasks.rb similarity index 100% rename from lib/probe_dock_ruby/tasks.rb rename to lib/probedock_ruby/tasks.rb diff --git a/lib/probe_dock_ruby/test_result.rb b/lib/probedock_ruby/test_result.rb similarity index 100% rename from lib/probe_dock_ruby/test_result.rb rename to lib/probedock_ruby/test_result.rb diff --git a/lib/probe_dock_ruby/test_run.rb b/lib/probedock_ruby/test_run.rb similarity index 100% rename from lib/probe_dock_ruby/test_run.rb rename to lib/probedock_ruby/test_run.rb diff --git a/lib/probe_dock_ruby/uid.rb b/lib/probedock_ruby/uid.rb similarity index 100% rename from lib/probe_dock_ruby/uid.rb rename to lib/probedock_ruby/uid.rb diff --git a/spec/config_spec.rb b/spec/config_spec.rb index 3ebe020..47a325d 100644 --- a/spec/config_spec.rb +++ b/spec/config_spec.rb @@ -114,8 +114,8 @@ config.load!(&config_block) actual_scm = %i(name version dirty).inject({}){ |memo,attr| memo[attr] = scm.send(attr); memo }.reject{ |k,v| v.nil? } - actual_scm[:remote] = scm.remote.to_h - actual_scm[:remote][:url] = scm.remote[:url].to_h + actual_scm[:remote] = %i(name ahead behind).inject({}){ |memo,attr| memo[attr] = scm.remote.send(attr); memo }.reject{ |k,v| v.nil? } + actual_scm[:remote][:url] = %i(fetch push).inject({}){ |memo,attr| memo[attr] = scm.remote.url.send(attr); memo }.reject{ |k,v| v.nil? } expect(actual_scm).to eq(expected_scm_configuration) end diff --git a/spec/configurable_spec.rb b/spec/configurable_spec.rb new file mode 100644 index 0000000..2cb4eb6 --- /dev/null +++ b/spec/configurable_spec.rb @@ -0,0 +1,191 @@ +require 'helper' + +describe ProbeDockProbe::Configurable do + Configurable ||= ProbeDockProbe::Configurable + + class OtherTest + include Configurable + + configurable({ + foo: :string + }) + end + + class Test + include Configurable + + configurable({ + foo: :boolean, + bar: :integer, + baz: :string, + qux: :string_array, + corge: OtherTest + }) + + attr_accessor :grault + end + + subject{ Test.new } + + it "should automatically initialize non-primitive attributes" do + expect(subject.foo).to be_nil + expect(subject.bar).to be_nil + expect(subject.baz).to be_nil + expect(subject.qux).to eq([]) + expect(subject.corge).to be_a_kind_of(OtherTest) + expect(subject.corge.foo).to be_nil + end + + it "should coerce attribute values" do + + subject.foo = 'bar' + expect(subject.foo).to be(true) + + subject.bar = '4' + expect(subject.bar).to eq(4) + + subject.bar = 'foo' + expect(subject.bar).to eq(0) + + subject.baz = 45.6 + expect(subject.baz).to eq('45.6') + + subject.baz = [ 'fo', 0, 'bar' ] + expect(subject.baz).to eq('["fo", 0, "bar"]') + + subject.qux = 'foo' + expect(subject.qux).to eq(%w(foo)) + + subject.qux = [ 1, true, 'bar' ] + expect(subject.qux).to eq(%w(1 true bar)) + + subject.corge = nil + expect(subject.corge).to be_a_kind_of(OtherTest) + expect(subject.corge).to be_empty + + subject.corge = { foo: 'bar' } + expect(subject.corge).to be_a_kind_of(OtherTest) + expect(subject.corge.foo).to eq('bar') + end + + describe "#empty?" do + it "should indicate whether configurable attributes are empty" do + + expect(subject).to be_empty + + subject.grault = 'foo' + expect(subject).to be_empty + + subject = Test.new + subject.foo = true + expect(subject).not_to be_empty + + subject = Test.new + subject.bar = 3 + expect(subject).not_to be_empty + + subject = Test.new + subject.baz = 'foo' + expect(subject).not_to be_empty + + subject = Test.new + subject.qux = %w(foo bar) + expect(subject).not_to be_empty + + subject = Test.new + subject.corge.foo = 'bar' + expect(subject).not_to be_empty + end + end + + describe "#update" do + it "should update configurable attributes with the specified hash" do + + subject.update({ + foo: true, + bar: 42, + baz: 'foo', + qux: %w(bar baz), + corge: { + foo: 'bar' + } + }) + + expect(subject.foo).to be(true) + expect(subject.bar).to eq(42) + expect(subject.baz).to eq('foo') + expect(subject.qux).to eq(%w(bar baz)) + expect(subject.corge).to be_a_kind_of(OtherTest) + expect(subject.corge.foo).to eq('bar') + end + + it "should not update attributes that are not in the hash" do + + subject.update({ + foo: true, + baz: 'foo', + corge: { + foo: 'bar' + } + }) + + expect(subject.foo).to be(true) + expect(subject.bar).to be_nil + expect(subject.baz).to eq('foo') + expect(subject.qux).to eq([]) + expect(subject.corge).to be_a_kind_of(OtherTest) + expect(subject.corge.foo).to eq('bar') + end + end + + describe "#clear" do + it "should clear all configurable attributes" do + + subject.foo = true + subject.bar = 42 + subject.baz = 'foo' + subject.qux = %w(bar baz) + subject.corge.foo = 'bar' + + subject.clear + + expect(subject.foo).to be_nil + expect(subject.bar).to be_nil + expect(subject.baz).to be_nil + expect(subject.qux).to eq([]) + expect(subject.corge).to be_a_kind_of(OtherTest) + expect(subject.corge.foo).to be_nil + end + end + + describe "#to_h" do + it "should serialize configurable attributes as a hash" do + + subject.foo = true + subject.bar = 42 + subject.baz = 'foo' + subject.qux = %w(bar baz) + subject.corge.foo = 'bar' + + expect(subject.to_h).to eq({ + foo: true, + bar: 42, + baz: 'foo', + qux: %w(bar baz), + corge: { + foo: 'bar' + } + }) + end + + it "should not serialize missing attributes" do + + subject.bar = 42 + + expect(subject.to_h).to eq({ + bar: 42, + corge: {} + }) + end + end +end diff --git a/spec/server_spec.rb b/spec/server_spec.rb index cd29322..1356ae8 100644 --- a/spec/server_spec.rb +++ b/spec/server_spec.rb @@ -2,6 +2,7 @@ describe ProbeDockProbe::Server do let(:api_token){ 'abcdefghijklmnopqrstuvwxyz' } + let :options do { name: 'A server', @@ -10,7 +11,9 @@ project_api_id: '0000000000' } end + let(:server){ ProbeDockProbe::Server.new options } + subject{ server } it "should set its attributes" do @@ -36,7 +39,8 @@ describe "#clear" do it "should clear the configuration" do server.clear - options.keys.each{ |k| expect(subject.send(k)).to be_nil } + expect(server.name).to eq(options[:name]) + (options.keys - %i(name)).each{ |k| expect(subject.send(k)).to be_nil } end end