Class: Kettle::Jem::DecisionPolicy

Inherits:
Object
  • Object
show all
Defined in:
lib/kettle/jem.rb

Constant Summary collapse

TRUE_VALUES =
%w[1 true y yes on].freeze
FALSE_VALUES =
%w[0 false n no off].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(mode: :accept, failure_mode: "error", require_clean: nil, input_source: "default", prompt_answers: {}) ⇒ DecisionPolicy

Returns a new instance of DecisionPolicy.



616
617
618
619
620
621
622
# File 'lib/kettle/jem.rb', line 616

def initialize(mode: :accept, failure_mode: "error", require_clean: nil, input_source: "default", prompt_answers: {})
  @mode = normalize_mode(mode)
  @failure_mode = failure_mode.to_s.empty? ? "error" : failure_mode.to_s
  @require_clean = require_clean
  @input_source = input_source.to_s
  @prompt_answers = normalize_prompt_answers(prompt_answers)
end

Instance Attribute Details

#failure_mode ⇒ Object (readonly)

Returns the value of attribute failure_mode.



565
566
567
# File 'lib/kettle/jem.rb', line 565

def failure_mode
  @failure_mode
end

#input_source ⇒ Object (readonly)

Returns the value of attribute input_source.



565
566
567
# File 'lib/kettle/jem.rb', line 565

def input_source
  @input_source
end

#mode ⇒ Object (readonly)

Returns the value of attribute mode.



565
566
567
# File 'lib/kettle/jem.rb', line 565

def mode
  @mode
end

#prompt_answers ⇒ Object (readonly)

Returns the value of attribute prompt_answers.



565
566
567
# File 'lib/kettle/jem.rb', line 565

def prompt_answers
  @prompt_answers
end

#require_clean ⇒ Object (readonly)

Returns the value of attribute require_clean.



565
566
567
# File 'lib/kettle/jem.rb', line 565

def require_clean
  @require_clean
end

Class Method Details

.falsey?(value) ⇒ Boolean

Returns:

  • (Boolean)


592
593
594
# File 'lib/kettle/jem.rb', line 592

def self.falsey?(value)
  FALSE_VALUES.include?(value.to_s.strip.downcase)
end

.from_env(env = {}, **options) ⇒ Object



567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
# File 'lib/kettle/jem.rb', line 567

def self.from_env(env = {}, **options)
  env_hash = env || {}
  option_hash = symbolize_keys(options)
  interactive = option_hash.key?(:interactive) ? option_hash[:interactive] : truthy?(env_hash["interactive"])
  force = option_hash.key?(:force) ? option_hash[:force] : value_to_boolean(env_hash["force"])
  accept = option_hash.key?(:accept) ? option_hash[:accept] : value_to_boolean(env_hash["accept"])
  interactive = false if accept == true || force == true
  interactive = true if force == false && accept != true && !option_hash.key?(:interactive)
  new(
    mode: interactive ? :interactive : :accept,
    failure_mode: option_hash.fetch(:failure_mode, env_hash["FAILURE_MODE"] || env_hash["failure_mode"] || "error"),
    require_clean: option_hash.fetch(:require_clean, value_to_boolean(env_hash["KETTLE_JEM_REQUIRE_CLEAN"])),
    input_source: option_hash.fetch(:input_source, "default"),
    prompt_answers: option_hash.fetch(:prompt_answers, parse_prompt_answers(env_hash["KETTLE_JEM_PROMPT_ANSWERS"]))
  )
end

.parse_prompt_answers(value) ⇒ Object



605
606
607
608
609
610
611
612
613
614
# File 'lib/kettle/jem.rb', line 605

def self.parse_prompt_answers(value)
  return {} if value.nil? || value.to_s.strip.empty?

  parsed = JSON.parse(value.to_s)
  raise ArgumentError, "KETTLE_JEM_PROMPT_ANSWERS must be a JSON object" unless parsed.is_a?(Hash)

  parsed
rescue JSON::ParserError => error
  raise ArgumentError, "KETTLE_JEM_PROMPT_ANSWERS must be valid JSON: #{error.message}"
end

.symbolize_keys(hash) ⇒ Object



584
585
586
# File 'lib/kettle/jem.rb', line 584

def self.symbolize_keys(hash)
  hash.each_with_object({}) { |(key, value), acc| acc[key.to_sym] = value }
end

.truthy?(value) ⇒ Boolean

Returns:

  • (Boolean)


588
589
590
# File 'lib/kettle/jem.rb', line 588

def self.truthy?(value)
  TRUE_VALUES.include?(value.to_s.strip.downcase)
end

.value_to_boolean(value) ⇒ Object



596
597
598
599
600
601
602
603
# File 'lib/kettle/jem.rb', line 596

def self.value_to_boolean(value)
  return true if value == true
  return false if value == false
  return true if truthy?(value)
  return false if falsey?(value)

  nil
end

Instance Method Details

#accept? ⇒ Boolean

Returns:

  • (Boolean)


624
625
626
# File 'lib/kettle/jem.rb', line 624

def accept?
  mode == :accept
end

#interactive? ⇒ Boolean

Returns:

  • (Boolean)


628
629
630
# File 'lib/kettle/jem.rb', line 628

def interactive?
  mode == :interactive
end

#non_interactive? ⇒ Boolean

Returns:

  • (Boolean)


632
633
634
# File 'lib/kettle/jem.rb', line 632

def non_interactive?
  !interactive?
end

#resolve(id:, category:, default_action:, file: nil, severity: :advisory, diagnostics: []) ⇒ Object

Raises:



636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
# File 'lib/kettle/jem.rb', line 636

def resolve(id:, category:, default_action:, file: nil, severity: :advisory, diagnostics: [])
  action = normalize_action(default_action)
  severity_value = normalize_severity(severity)
  raise Error, "No safe default decision for #{id}" if action.nil? && severity_value == "fatal"
  promptable = interactive? && !action.nil? && severity_value != "fatal"
  answer = promptable ? prompt_answers[id.to_s] : nil
  selected_action = answer || action
  selected_source = if answer
    "interactive_answer"
  elsif promptable
    "interactive_default"
  else
    "default"
  end
  decision_diagnostics = Array(diagnostics).compact.map(&:to_s)
  prompt = nil
  if promptable
    prompt = {
      id: id.to_s,
      category: category.to_s,
      file: file&.to_s,
      message: "Apply #{category.to_s.tr("_", " ")} for #{file || id}?",
      default_action: action,
      choices: DECISION_ACTIONS
    }.compact
    decision_diagnostics << if answer
      "Interactive prompt answer supplied through the shared decision policy input contract."
    else
      "Interactive prompt transport is active; selected the configured default pending an external response."
    end
  end

  DecisionEvaluation.new(
    id: id.to_s,
    category: category.to_s,
    file: file&.to_s,
    default_action: action,
    selected_action: selected_action,
    source: selected_source,
    severity: severity_value,
    blocking: severity_value == "fatal",
    diagnostics: decision_diagnostics,
    prompt_required: promptable,
    prompt: prompt
  )
end

#to_h ⇒ Object



683
684
685
686
687
688
689
690
691
692
693
694
# File 'lib/kettle/jem.rb', line 683

def to_h
  {
    mode: mode.to_s,
    non_interactive: non_interactive?,
    accept: accept?,
    interactive: interactive?,
    failure_mode: failure_mode,
    require_clean: require_clean,
    input_source: input_source,
    prompt_answers: prompt_answers.empty? ? nil : prompt_answers
  }.compact
end