From 04cfc577b8e2ff0ab5713a5a8c54e728e6a087d4 Mon Sep 17 00:00:00 2001 From: Laura Montemayor Date: Thu, 16 Oct 2025 15:37:32 +0200 Subject: [PATCH] Add RulesConverter to convert expression nodes into JSON --- lib/gitlab/ci/inputs/rules_converter.rb | 56 +++++++++ .../gitlab/ci/inputs/rules_converter_spec.rb | 108 ++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 lib/gitlab/ci/inputs/rules_converter.rb create mode 100644 spec/lib/gitlab/ci/inputs/rules_converter_spec.rb diff --git a/lib/gitlab/ci/inputs/rules_converter.rb b/lib/gitlab/ci/inputs/rules_converter.rb new file mode 100644 index 00000000000000..447c2bf7b9c5f2 --- /dev/null +++ b/lib/gitlab/ci/inputs/rules_converter.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Inputs + ## + # + # Converts parsed expression AST nodes JSON format with operator, field, value, and children. + # + class RulesConverter + def convert(node) + return unless node + + case node + when Gitlab::Ci::Pipeline::Expression::Lexeme::Equals + { + 'operator' => 'equals', + 'field' => extract_input_name(node.left), + 'value' => extract_value(node.right) + } + when Gitlab::Ci::Pipeline::Expression::Lexeme::NotEquals + { + 'operator' => 'not_equals', + 'field' => extract_input_name(node.left), + 'value' => extract_value(node.right) + } + when Gitlab::Ci::Pipeline::Expression::Lexeme::And + { + 'operator' => 'AND', + 'children' => [convert(node.left), convert(node.right)].compact + } + when Gitlab::Ci::Pipeline::Expression::Lexeme::Or + { + 'operator' => 'OR', + 'children' => [convert(node.left), convert(node.right)].compact + } + end + end + + private + + def extract_input_name(node) + return unless node.is_a?(Gitlab::Ci::Pipeline::Expression::Lexeme::Input) + + node.value + end + + def extract_value(node) + return unless node.is_a?(Gitlab::Ci::Pipeline::Expression::Lexeme::String) + + node.evaluate + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/inputs/rules_converter_spec.rb b/spec/lib/gitlab/ci/inputs/rules_converter_spec.rb new file mode 100644 index 00000000000000..fa4450c33584b8 --- /dev/null +++ b/spec/lib/gitlab/ci/inputs/rules_converter_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Inputs::RulesConverter, feature_category: :pipeline_composition do + let(:converter) { described_class.new } + + describe '#convert' do + context 'with nil node' do + it 'returns nil' do + expect(converter.convert(nil)).to be_nil + end + end + + context 'with equals operator' do + it 'converts to equals structure' do + statement = Gitlab::Ci::Pipeline::Expression::Statement.new('$[[ inputs.env ]] == "prod"') + node = statement.parse_tree + + result = converter.convert(node) + + expect(result).to eq( + 'operator' => 'equals', + 'field' => 'env', + 'value' => 'prod' + ) + end + end + + context 'with not_equals operator' do + it 'converts to not_equals structure' do + statement = Gitlab::Ci::Pipeline::Expression::Statement.new('$[[ inputs.env ]] != "dev"') + node = statement.parse_tree + + result = converter.convert(node) + + expect(result).to eq( + 'operator' => 'not_equals', + 'field' => 'env', + 'value' => 'dev' + ) + end + end + + context 'with AND operator' do + it 'converts to AND structure with children' do + statement = Gitlab::Ci::Pipeline::Expression::Statement.new( + '$[[ inputs.env ]] == "prod" && $[[ inputs.region ]] == "us"' + ) + node = statement.parse_tree + + result = converter.convert(node) + + expect(result).to eq( + 'operator' => 'AND', + 'children' => [ + { 'operator' => 'equals', 'field' => 'env', 'value' => 'prod' }, + { 'operator' => 'equals', 'field' => 'region', 'value' => 'us' } + ] + ) + end + end + + context 'with OR operator' do + it 'converts to OR structure with children' do + statement = Gitlab::Ci::Pipeline::Expression::Statement.new( + '$[[ inputs.env ]] == "prod" || $[[ inputs.env ]] == "staging"' + ) + node = statement.parse_tree + + result = converter.convert(node) + + expect(result).to eq( + 'operator' => 'OR', + 'children' => [ + { 'operator' => 'equals', 'field' => 'env', 'value' => 'prod' }, + { 'operator' => 'equals', 'field' => 'env', 'value' => 'staging' } + ] + ) + end + end + + context 'with nested operators' do + it 'converts nested AND/OR correctly' do + statement = Gitlab::Ci::Pipeline::Expression::Statement.new( + '($[[ inputs.env ]] == "prod" && $[[ inputs.region ]] == "us") || $[[ inputs.env ]] == "dev"' + ) + node = statement.parse_tree + + result = converter.convert(node) + + expect(result).to eq( + 'operator' => 'OR', + 'children' => [ + { + 'operator' => 'AND', + 'children' => [ + { 'operator' => 'equals', 'field' => 'env', 'value' => 'prod' }, + { 'operator' => 'equals', 'field' => 'region', 'value' => 'us' } + ] + }, + { 'operator' => 'equals', 'field' => 'env', 'value' => 'dev' } + ] + ) + end + end + end +end -- GitLab