Commit db63248d authored by Peter Leitzen's avatar Peter Leitzen Committed by Vitali Tatarintev

Implement payload field extract service and model

Parses an alert's paylad and provide the list possible fields and their
types
parent 2de65767
# frozen_string_literal: true
module AlertManagement
class AlertPayloadField
include ActiveModel::Model
include ActiveModel::Validations
attr_accessor :project, :path, :label, :type
validates :project, presence: true
validates :path, presence: true
validates :label, presence: true
validates :type, presence: true
end
end
# frozen_string_literal: true
module AlertManagement
class ExtractAlertPayloadFieldsService < BaseContainerService
alias_method :project, :container
def execute
return error('Feature not available') unless available?
return error('Insufficient permissions') unless allowed?
payload = parse_payload
return error('Failed to parse payload') unless payload && payload.is_a?(Hash)
return error('Payload size exceeded') unless valid_payload_size?(payload)
fields = Gitlab::AlertManagement::AlertPayloadFieldExtractor
.new(project).extract(payload)
success(fields)
end
private
def parse_payload
Gitlab::Json.parse(params[:payload])
rescue JSON::ParserError
end
def valid_payload_size?(payload)
Gitlab::Utils::DeepSize.new(payload).valid?
end
def success(fields)
ServiceResponse.success(payload: { payload_alert_fields: fields })
end
def error(message)
ServiceResponse.error(message: message)
end
def available?
feature_enabled? && license_available?
end
def allowed?
current_user&.can?(:admin_operations, project)
end
def feature_enabled?
Feature.enabled?(:multiple_http_integrations_custom_mapping, project)
end
def license_available?
project&.feature_available?(:multiple_alert_http_integrations)
end
end
end
# frozen_string_literal: true
module Gitlab
module AlertManagement
class AlertPayloadFieldExtractor
def initialize(project)
@project = project
end
def extract(payload)
deep_traverse(payload.deep_stringify_keys)
.map { |path, value| field(path, value) }
.compact
end
private
attr_reader :project
def field(path, value)
type = type_of(value)
return unless type
label = path.last.humanize
::AlertManagement::AlertPayloadField.new(
project: project,
path: path,
label: label,
type: type
)
end
# TODO: Code duplication with Gitlab::InlineHash#merge_keys ahead!
def deep_traverse(hash)
return to_enum(__method__, hash) unless block_given?
pairs = hash.map { |k, v| [[k], v] }
until pairs.empty?
key, value = pairs.shift
if value.is_a?(Hash)
value.each { |k, v| pairs.unshift [key + [k], v] }
else
yield key, value
end
end
end
def type_of(value)
case value
when /^\d{4}/ # assume it's a datetime
'datetime'
when String
'string'
when Numeric
'numeric'
end
end
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :alert_management_alert_payload_field, class: 'AlertManagement::AlertPayloadField' do
project
path { 'title' }
label { 'Title' }
type { 'string' }
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::AlertManagement::AlertPayloadFieldExtractor do
let(:project) { build_stubbed(:project) }
let(:extractor) { described_class.new(project) }
let(:payload) { {} }
let(:json) { Gitlab::Json.parse(Gitlab::Json.generate(payload)) }
let(:field) { fields.first }
subject(:fields) { extractor.extract(json) }
context 'plain' do
before do
payload.merge!(
str: 'value',
int: 23,
float: 23.5,
nested: {
key: 'level1',
deep: {
key: 'level2'
}
},
time: '2020-12-09T12:34:56',
discarded_null: nil,
discarded_bool_true: true,
discarded_bool_false: false
)
end
it 'works' do
expect(fields).to contain_exactly(
a_field(['str'], 'Str', 'string'),
a_field(['int'], 'Int', 'numeric'),
a_field(['float'], 'Float', 'numeric'),
a_field(%w(nested key), 'Key', 'string'),
a_field(%w(nested deep key), 'Key', 'string'),
a_field(['time'], 'Time', 'datetime')
)
end
end
private
def a_field(path, label, type)
have_attributes(project: project, path: path, label: label, type: type)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe AlertManagement::AlertPayloadField do
describe 'Validations' do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:path) }
it { is_expected.to validate_presence_of(:label) }
it { is_expected.to validate_presence_of(:type) }
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe AlertManagement::ExtractAlertPayloadFieldsService do
let(:project) { build_stubbed(:project) }
let(:user) { build_stubbed(:user) }
let(:params) { { payload: payload_json } }
let(:payload_json) { Gitlab::Json.generate(payload) }
let(:payload) { { foo: 'bar' } }
let(:service) do
described_class.new(container: project, current_user: user, params: params)
end
subject(:response) { service.execute }
before do
stub_licensed_features(multiple_alert_http_integrations: true)
allow(user).to receive(:can?).with(:admin_operations, project).and_return(true)
end
it 'works' do
expect(response).to be_success
end
context 'fails when limits are exceeded'
context 'fails with invalid payload'
context 'without license'
context 'without feature flag'
context 'without permission'
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment