Class: Sxn::Rules::BaseRule

Inherits:
Object
  • Object
show all
Includes:
States
Defined in:
lib/sxn/rules/base_rule.rb

Overview

BaseRule is the abstract base class for all rule types in the sxn system. It defines the common interface that all rules must implement and provides shared functionality for validation, dependency management, and error handling.

Rules are the building blocks of session setup automation. They can copy files, execute commands, process templates, or perform other project initialization tasks.

Examples:

Implementing a custom rule

class MyCustomRule < BaseRule
  def validate
    raise ValidationError, "Custom validation failed" unless valid?
  end

  def apply
    # Perform the rule's action
    track_change(:file_created, "/path/to/file")
  end

  def rollback
    # Undo the rule's action
    File.unlink("/path/to/file") if File.exist?("/path/to/file")
  end
end

Direct Known Subclasses

CopyFilesRule, SetupCommandsRule, TemplateRule

Defined Under Namespace

Modules: States Classes: RuleChange

Constant Summary

Constants included from States

States::APPLIED, States::APPLYING, States::FAILED, States::PENDING, States::ROLLED_BACK, States::ROLLING_BACK, States::VALIDATED, States::VALIDATING

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(arg1 = nil, arg2 = nil, arg3 = nil, arg4 = nil, dependencies: []) ⇒ BaseRule

Initialize a new rule instance



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/sxn/rules/base_rule.rb', line 53

def initialize(arg1 = nil, arg2 = nil, arg3 = nil, arg4 = nil, dependencies: [])
  # Handle both old and new initialization formats
  if (arg1.is_a?(String) || arg1.nil?) && arg2.is_a?(Hash) && arg3.is_a?(String) && arg4.is_a?(String)
    # Old format: (name, config, project_path, session_path, dependencies: [])
    @name = arg1 || "base_rule"
    @config = arg2.dup.freeze
    @project_path = File.realpath(arg3)
    @session_path = File.realpath(arg4)
  elsif arg1.is_a?(Hash) && arg2.is_a?(String) && arg3.is_a?(String)
    # Special format: (config, project_path, session_path, name)
    @name = arg4 || "base_rule"
    @config = arg1.dup.freeze
    @project_path = File.realpath(arg2)
    @session_path = File.realpath(arg3)
  elsif arg1.is_a?(String) && arg2.is_a?(String)
    # New format: (project_path, session_path, config = {}, dependencies: [])
    @name = "base_rule"
    # Store the config as-is for validation, only freeze if it's a Hash
    @config = if arg3.nil?
                {}.freeze
              elsif arg3.is_a?(Hash)
                arg3.dup.freeze
              else
                # Store non-hash config as-is for validation to catch
                arg3
              end
    @project_path = File.realpath(arg1)
    @session_path = File.realpath(arg2)
  else
    raise ArgumentError,
          "Invalid arguments. Expected (name, config, project_path, session_path) or (project_path, session_path, config={})"
  end

  @dependencies = dependencies.freeze
  @state = PENDING
  @changes = []
  @errors = []
  @start_time = nil
  @end_time = nil

  validate_paths!
rescue Errno::ENOENT => e
  raise ArgumentError, "Invalid path provided: #{e.message}"
end

Instance Attribute Details

#changesObject (readonly)

Returns the value of attribute changes.



44
45
46
# File 'lib/sxn/rules/base_rule.rb', line 44

def changes
  @changes
end

#configObject (readonly)

Returns the value of attribute config.



44
45
46
# File 'lib/sxn/rules/base_rule.rb', line 44

def config
  @config
end

#dependenciesObject (readonly)

Returns the value of attribute dependencies.



44
45
46
# File 'lib/sxn/rules/base_rule.rb', line 44

def dependencies
  @dependencies
end

#errorsObject (readonly)

Returns the value of attribute errors.



44
45
46
# File 'lib/sxn/rules/base_rule.rb', line 44

def errors
  @errors
end

#nameObject (readonly)

Returns the value of attribute name.



44
45
46
# File 'lib/sxn/rules/base_rule.rb', line 44

def name
  @name
end

#project_pathObject (readonly)

Returns the value of attribute project_path.



44
45
46
# File 'lib/sxn/rules/base_rule.rb', line 44

def project_path
  @project_path
end

#session_pathObject (readonly)

Returns the value of attribute session_path.



44
45
46
# File 'lib/sxn/rules/base_rule.rb', line 44

def session_path
  @session_path
end

#stateObject (readonly)

Returns the value of attribute state.



44
45
46
# File 'lib/sxn/rules/base_rule.rb', line 44

def state
  @state
end

Instance Method Details

#applied?Boolean

Check if rule has been successfully applied



202
203
204
# File 'lib/sxn/rules/base_rule.rb', line 202

def applied?
  @state == APPLIED
end

#apply(context = {}) ⇒ Boolean

Apply the rule’s action This method must be overridden by subclasses to implement the actual rule logic

Raises:



126
127
128
# File 'lib/sxn/rules/base_rule.rb', line 126

def apply(context = {})
  raise NotImplementedError, "#{self.class} must implement #apply"
end

#can_execute?(completed_rules) ⇒ Boolean

Check if this rule can be executed (all dependencies are satisfied)



155
156
157
# File 'lib/sxn/rules/base_rule.rb', line 155

def can_execute?(completed_rules)
  @dependencies.all? { |dep| completed_rules.include?(dep) }
end

#descriptionString

Get rule description



195
196
197
# File 'lib/sxn/rules/base_rule.rb', line 195

def description
  "Base rule for #{type} operations"
end

#durationFloat?

Get rule execution duration in seconds



162
163
164
165
166
# File 'lib/sxn/rules/base_rule.rb', line 162

def duration
  return nil unless @start_time && @end_time

  @end_time - @start_time
end

#failed?Boolean

Check if rule has failed



209
210
211
# File 'lib/sxn/rules/base_rule.rb', line 209

def failed?
  @state == FAILED
end

#required?Boolean

Check if rule is required



178
179
180
# File 'lib/sxn/rules/base_rule.rb', line 178

def required?
  true
end

#rollbackBoolean

Rollback the rule’s changes This method should be overridden by subclasses to implement rollback logic

Raises:



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/sxn/rules/base_rule.rb', line 135

def rollback
  return true if @state == PENDING || @state == FAILED

  change_state!(ROLLING_BACK)

  begin
    rollback_changes!
    change_state!(ROLLED_BACK)
    true
  rescue StandardError => e
    @errors << e
    change_state!(FAILED)
    raise Sxn::Rules::RollbackError, "Failed to rollback rule #{@name}: #{e.message}"
  end
end

#rollbackable?Boolean

Check if rule can be rolled back



216
217
218
# File 'lib/sxn/rules/base_rule.rb', line 216

def rollbackable?
  @state == APPLIED && @changes.any?
end

#to_hHash

Get a hash representation of the rule for serialization



223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/sxn/rules/base_rule.rb', line 223

def to_h
  {
    name: @name,
    type: self.class.name.split("::").last,
    state: @state,
    config: @config,
    dependencies: @dependencies,
    changes: @changes.map(&:to_h),
    errors: @errors.map(&:message),
    duration: duration,
    applied_at: @end_time&.iso8601
  }
end

#typeString

Get rule type



171
172
173
# File 'lib/sxn/rules/base_rule.rb', line 171

def type
  self.class.name.split("::").last.downcase.gsub(/rule$/, "")
end

#validateBoolean

Validate the rule configuration and dependencies This method should be overridden by subclasses to implement specific validation logic

Raises:



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/sxn/rules/base_rule.rb', line 103

def validate
  change_state!(VALIDATING)

  begin
    validate_config!
    validate_dependencies!
    validate_rule_specific!

    change_state!(VALIDATED)
    true
  rescue StandardError => e
    @errors << e
    change_state!(FAILED)
    raise
  end
end

#validate_config_hash(config = @config) ⇒ Boolean

Validate rule configuration (public method expected by tests)



186
187
188
189
190
# File 'lib/sxn/rules/base_rule.rb', line 186

def validate_config_hash(config = @config)
  return true if config.nil? || config.empty?

  config.is_a?(Hash)
end