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

Parameters:

  • name (String)

    Unique name for this rule instance (old format) or project_path (new format)

  • config_or_session_path (Hash|String)

    Rule configuration (old) or session_path (new)

  • project_path (String)

    Absolute path to the project root (old format)

  • session_path (String)

    Absolute path to the session directory (old format)

  • dependencies (Array<String>) (defaults to: [])

    Names of rules this rule depends on



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

Returns:

  • (Boolean)

    true if rule is in applied state



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

Parameters:

  • context (Hash) (defaults to: {})

    Optional execution context

Returns:

  • (Boolean)

    true if application succeeds

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)

Parameters:

  • completed_rules (Array<String>)

    List of rule names that have been completed

Returns:

  • (Boolean)

    true if 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

Returns:

  • (String)

    Description of the rule



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

Returns:

  • (Float, nil)

    Execution duration or nil if not completed



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

Returns:

  • (Boolean)

    true if rule is in failed state



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

def failed?
  @state == FAILED
end

#required?Boolean

Check if rule is required

Returns:

  • (Boolean)

    true 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

Returns:

  • (Boolean)

    true if rollback succeeds

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

Returns:

  • (Boolean)

    true 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

Returns:

  • (Hash)

    Rule data



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

Returns:

  • (String)

    Rule type based on class name



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

Returns:

  • (Boolean)

    true if validation passes

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)

Parameters:

  • config (Hash) (defaults to: @config)

    Configuration to validate

Returns:

  • (Boolean)

    true if valid



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