Search code examples
jsonnet

Complex validation in Jsonnet library


What I'm trying to do is create a libsonnet library with some complex validation on the inputs, but I'm not sure how to implement this in the libsonnet file without getting null back.

I'm trying to generate API calls for Hosted Graphite's Alerts API using Jsonnet. The idea being is that we can store all of our alerts in version control, and update them in a CI / CD pipeline. I want to prevent errors, so I've implemented some complex validation based on what the above API spec defines. I have the following saved as alerts.libsonnet:

local alert_criteria_types = [
  'above',
  'below',
  'missing',
  'outside_bounds',
];

local notification_types_strings = [
  'state_change',
];

local notification_types_arrays = [
  'every',
  'state_change',
];

local on_query_failure_types = [
  'ignore',
  'notify',
  null,
];

{
  local HostedGraphiteAlerts = self,

  new(
    name,
    metric,
    alert_criteria_type,
    additional_alert_criteria={},
    additional_criteria={},
    expression='a',
    scheduled_mutes=[],
    notification_channels=['Email me'],
    notification_type=null,
    info=null,
    on_query_failure='notify',
  )::

    // Simple checks
    assert std.member(alert_criteria_types, alert_criteria_type) : "Input 'alert_criteria_type' is not one of the types: %s." % std.join(', ', alert_criteria_types);
    assert std.member(on_query_failure_types, on_query_failure) : "Input 'on_query_failure_type' is not one of the types: %s." % std.join(', ', on_query_failure_types);

    // Advanced checks
    if notification_type != null && std.isString(notification_type) then
        assert std.member(notification_types_strings, notification_type) : "Input 'notification_type' is not one of the types: %s." % std.join(', ', notification_types_strings);

    if notification_type != null && std.isArray(notification_type) then
      assert std.member(notification_types_arrays, notification_type[0]) : "Input 'notification_type' is not one of the types: %s." % std.join(', ', notification_types_arrays);

    if notification_type != null && std.isArray(notification_type) then
        assert std.member(notification_types_arrays, notification_type[0]) : "Input 'notification_type' is not one of the types: %s." % std.join(', ', notification_types_arrays);

    if notification_type != null && std.isArray(notification_type) && notification_type[0] == 'every' then
        assert notification_type[1] != null : "Input 'notification_type' cannot have an empty entry for 'time_in_minutes' for notification type 'every'.";

    if notification_type != null && std.isArray(notification_type) && notification_type[0] == 'every' then
        assert std.isNumber(notification_type[1]) : "Input 'notification_type' must have a JSON 'number' type for notification type 'every'.";

    // Main
    {
      name: name,
      metric: metric,
      alert_criteria: {
        type: alert_criteria_type,
      } + additional_alert_criteria,
      additional_criteria: additional_criteria,
      expression: expression,
      scheduled_mutes: scheduled_mutes,
      notification_channels: notification_channels,
      notification_type: notification_type,
      info: info,
      on_query_failure: on_query_failure,
    },
}

This passes a basic jsonnetfmt check, but the problem is when I go to use it in an alerts.jsonnet file like so:

local alerts = (import 'hosted_graphite.libsonnet').alerts;

alerts.new(
  name='something',
  metric='some.graphite.metric',
  alert_criteria_type='below',
)

This simply returns null:

$ jsonnet hosted_graphite/alerts.jsonnet
null

I know this is because it's taking the value of the first assert statement. But how else can this be done?

Thank you!


Solution

  • Beware that jsonnet is not an imperative language, don't expect those if lines to get evaluated as if where part of a script. Think of assertions as a "virtual" / invisible field that must always evaluate to true

    Below implements what (I think) you're after:

    hosted_graphite.libsonnet

    local alert_criteria_types = [
      'above',
      'below',
      'missing',
      'outside_bounds',
    ];
    
    local notification_types_strings = [
      'state_change',
    ];
    
    local notification_types_arrays = [
      'every',
      'state_change',
    ];
    
    local on_query_failure_types = [
      'ignore',
      'notify',
      null,
    ];
    
    {
      local HostedGraphiteAlerts = self,
    
      new(
        name,
        metric,
        alert_criteria_type,
        additional_alert_criteria={},
        additional_criteria={},
        expression='a',
        scheduled_mutes=[],
        notification_channels=['Email me'],
        notification_type=null,
        info=null,
        on_query_failure='notify',
      )::
    
        // Main
        {
          name: name,
          metric: metric,
          alert_criteria: {
            type: alert_criteria_type,
          } + additional_alert_criteria,
          additional_criteria: additional_criteria,
          expression: expression,
          scheduled_mutes: scheduled_mutes,
          notification_channels: notification_channels,
          notification_type: notification_type,
          info: info,
          on_query_failure: on_query_failure,
    
          // Simple checks
          assert std.member(alert_criteria_types, self.alert_criteria.type) : (
            "Input 'alert_criteria_type' is not one of the types: %s." % std.join(', ', alert_criteria_types)
          ),
          assert std.member(on_query_failure_types, self.on_query_failure) : (
            "Input 'on_query_failure_type' is not one of the types: %s." % std.join(', ', on_query_failure_types)
          ),
    
          // Advanced checks:
          // - 1st line is a conditional that must be false ('A||B' construct) to get 2nd line evaluated
          // - 2nd line is the "final" type/value check, must be true
          assert (self.notification_type == null || !std.isString(self.notification_type) ||
                  std.member(notification_types_strings, self.notification_type)) : (
            "Input 'notification_type' string is not one of the types: %s." % std.join(', ', notification_types_strings)
          ),
          assert (self.notification_type == null || !std.isArray(self.notification_type) ||
                  std.member(notification_types_arrays, self.notification_type[0])) : (
            "Input 'notification_type' array is not one of the types: %s." % std.join(', ', notification_types_arrays)
          ),
          assert (self.notification_type == null || !std.isArray(self.notification_type) ||
                  self.notification_type != ['every', null]) : (
            "Input 'notification_type' cannot have an empty entry for 'time_in_minutes' for notification type 'every'."
          ),
          assert (self.notification_type == null || !std.isArray(self.notification_type) ||
                  [self.notification_type[0], std.isNumber(self.notification_type[1])] == ['every', true]) : (
            "Input 'notification_type' must have a JSON 'number' type for notification type 'every'."
          ),
        },
    }
    

    alerts.jsonnet

    local alerts = (import 'hosted_graphite.libsonnet');
    
    {
      a0: alerts.new(
        name='something',
        metric='some.graphite.metric',
        alert_criteria_type='below',
      ),
      a1: alerts.new(
        name='something',
        metric='some.graphite.metric',
        alert_criteria_type='below',
        notification_type='state_change',
      ),
      a2: alerts.new(
        name='something',
        metric='some.graphite.metric',
        alert_criteria_type='below',
        notification_type=['every', 10],
      ),
    }
    

    Note that I'm using self.<field> rather than the function parameter, it's a good pattern to allow derivation/overriding while still getting the assertions evaluated.

    BTW I'd also recommend looking at https://cuelang.org/, which plays in the same field as jsonnet but with type-checking being integral part of the language.