diff --git a/lib/gitlab/ci/config/extendable.rb b/lib/gitlab/ci/config/extendable.rb deleted file mode 100644 index dea0dbdb44b..00000000000 --- a/lib/gitlab/ci/config/extendable.rb +++ /dev/null @@ -1,31 +0,0 @@ -module Gitlab - module Ci - class Config - class Extendable - include Enumerable - - ExtensionError = Class.new(StandardError) - - def initialize(hash) - @hash = hash - end - - def each - @hash.each_pair do |key, value| - next unless value.key?(:extends) - - yield key, value.fetch(:extends).to_sym, value - end - end - - def extend! - @hash.tap do - each do |key, extends, value| - @hash[key] = @hash.fetch(extends).deep_merge(value) - end - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/extendable/collection.rb b/lib/gitlab/ci/config/extendable/collection.rb new file mode 100644 index 00000000000..4ffc4d91c82 --- /dev/null +++ b/lib/gitlab/ci/config/extendable/collection.rb @@ -0,0 +1,34 @@ +module Gitlab + module Ci + class Config + module Extendable + class Collection + include Enumerable + + ExtensionError = Class.new(StandardError) + + def initialize(hash, context = hash) + @hash = hash + @context = context + end + + def each + @hash.each_pair do |key, value| + next unless value.key?(:extends) + + yield Extendable::Entry.new(key, value, @context) + end + end + + def extend! + each do |entry| + raise ExtensionError unless entry.valid? + + @hash[entry.key] = entry.extend! + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/extendable/entry.rb b/lib/gitlab/ci/config/extendable/entry.rb new file mode 100644 index 00000000000..a39fa127e80 --- /dev/null +++ b/lib/gitlab/ci/config/extendable/entry.rb @@ -0,0 +1,51 @@ +module Gitlab + module Ci + class Config + module Extendable + class Entry + attr_reader :key + + def initialize(key, value, context, parent = nil) + @key = key + @value = value + @context = context + @parent = parent + end + + def valid? + true + end + + # def circular_dependency? + # @extends.to_s == @key.to_s + # end + + def base + Extendable::Entry + .new(extends, @context.fetch(extends), @context, self) + .extend! + end + + def extensible? + @value.key?(:extends) + end + + def extends + @value.fetch(:extends).to_sym + end + + def extend! + if extensible? + original = @value.dup + parent = base.dup + + @value.clear.deep_merge!(parent).deep_merge!(original) + else + @value.to_h + end + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/extendable/collection_spec.rb b/spec/lib/gitlab/ci/config/extendable/collection_spec.rb new file mode 100644 index 00000000000..453c7357561 --- /dev/null +++ b/spec/lib/gitlab/ci/config/extendable/collection_spec.rb @@ -0,0 +1,122 @@ +require 'fast_spec_helper' + +describe Gitlab::Ci::Config::Extendable::Collection do + subject { described_class.new(hash) } + + describe '#each' do + context 'when there is extendable entry in the hash' do + let(:test) do + { extends: 'something', only: %w[master] } + end + + let(:hash) do + { something: { script: 'ls' }, test: test } + end + + it 'yields the test hash' do + expect { |b| subject.each(&b) }.to yield_control + end + end + + context 'when not extending using a hash' do + let(:hash) do + { something: { extends: [1], script: 'ls' } } + end + + it 'yields invalid extends as well' do + expect { |b| subject.each(&b) }.to yield_control + end + end + end + + describe '#extend!' do + context 'when a hash has a single simple extension' do + let(:hash) do + { + something: { + script: 'deploy', + only: { variables: %w[$SOMETHING] } + }, + + test: { + extends: 'something', + script: 'ls', + only: { refs: %w[master] } + } + } + end + + it 'extends a hash with a deep reverse merge' do + expect(subject.extend!).to eq( + something: { + script: 'deploy', + only: { variables: %w[$SOMETHING] } + }, + + test: { + extends: 'something', + script: 'ls', + only: { + refs: %w[master], + variables: %w[$SOMETHING] + } + } + ) + end + end + + context 'when a hash uses recursive extensions' do + let(:hash) do + { + test: { + extends: 'something', + script: 'ls', + only: { refs: %w[master] } + }, + + something: { + extends: '.first', + script: 'deploy', + only: { variables: %w[$SOMETHING] } + }, + + '.first': { + script: 'run', + only: { kubernetes: 'active' } + } + } + end + + it 'extends a hash with a deep reverse merge' do + expect(subject.extend!).to eq( + '.first': { + script: 'run', + only: { kubernetes: 'active' } + }, + + something: { + extends: '.first', + script: 'deploy', + only: { + kubernetes: 'active', + variables: %w[$SOMETHING] + } + }, + + test: { + extends: 'something', + script: 'ls', + only: { + refs: %w[master], + variables: %w[$SOMETHING], + kubernetes: 'active' + } + } + ) + end + end + + pending 'when invalid `extends` is specified' + pending 'when circular dependecy has been detected' + end +end diff --git a/spec/lib/gitlab/ci/config/extendable_spec.rb b/spec/lib/gitlab/ci/config/extendable_spec.rb deleted file mode 100644 index a23fe560202..00000000000 --- a/spec/lib/gitlab/ci/config/extendable_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -require 'fast_spec_helper' - -describe Gitlab::Ci::Config::Extendable do - subject { described_class.new(hash) } - - describe '#each' do - context 'when there is extendable entry in the hash' do - let(:test) do - { extends: 'something', only: %w[master] } - end - - let(:hash) do - { something: { script: 'ls' }, test: test } - end - - it 'yields the test hash' do - expect { |b| subject.each(&b) } - .to yield_with_args(:test, :something, test) - end - end - - pending 'when not extending using a hash' - end - - describe '#extend!' do - context 'when a hash has a single simple extension' do - let(:hash) do - { - something: { - script: 'deploy', - only: { variables: %w[$SOMETHING] } - }, - test: { - extends: 'something', - script: 'ls', - only: { refs: %w[master] } - } - } - end - - it 'extends a hash with reverse merge' do - expect(subject.extend!).to eq( - something: { - script: 'deploy', - only: { variables: %w[$SOMETHING] } - }, - test: { - extends: 'something', - script: 'ls', - only: { - refs: %w[master], - variables: %w[$SOMETHING] - } - } - ) - end - end - - pending 'when a hash recursive extensions' - - pending 'when invalid `extends` is specified' - end -end