summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-01 23:21:20 +0200
committerPaul Buetow <paul@buetow.org>2026-03-01 23:21:20 +0200
commitfba26f6d9f18600dc313b6d4ade65d536e9762e9 (patch)
treed10d582416c5a1e831de7eae220d16cc280919ce
parent85f1805bea38d5f1558c92ff354f2d5bf832f0e6 (diff)
refactor: narrow BasicFile interface — apply ISP to Touch and Directory
Touch and Directory inherited content/from from BaseFile but had no use for them. Directory worked around this by repurposing content() as a source-directory path store, which was semantically misleading. Changes: - Move content/from down into BaseFile (only File and Symlink need them) - Move evaluate_absent! up into BasicFile (Touch and Directory need it) - Move UnsupportedOperation up into BasicFile (validate raises it there) - Re-parent Touch and Directory to BasicFile directly - Add Directory#source accessor to replace the content() misuse - Update DSL#directory to call d.source(...) instead of d.content(...) All 29 tests continue to pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--lib/dslkeywords/directory.rb26
-rw-r--r--lib/dslkeywords/file.rb55
-rw-r--r--lib/dslkeywords/touch.rb4
3 files changed, 53 insertions, 32 deletions
diff --git a/lib/dslkeywords/directory.rb b/lib/dslkeywords/directory.rb
index 402bead..6449d74 100644
--- a/lib/dslkeywords/directory.rb
+++ b/lib/dslkeywords/directory.rb
@@ -5,9 +5,15 @@ require_relative 'file'
module RCM
# Manages directories: create, delete/purge, or recursively copy from
# a source directory. Backup is performed before destructive operations.
- class Directory < BaseFile
+ # Extends BasicFile directly — Directory has no file content or sourcing,
+ # so it must not inherit content/from from BaseFile (ISP). The source
+ # directory for recursive copy is stored via the separate #source method.
+ class Directory < BasicFile
def recursively = @recursively = true
+ # Set or get the source directory path used for recursive copy.
+ def source(path = nil) = path.nil? ? @source_path : @source_path = path
+
def evaluate!
return unless super
@@ -35,6 +41,8 @@ module RCM
end
end
+ # Override BasicFile#evaluate_absent! with directory-specific behaviour:
+ # optionally recursive removal and backup of the whole directory tree.
def evaluate_absent!
return unless ::File.directory?(@file_path)
@@ -51,20 +59,20 @@ module RCM
end
def evaluate_present_recursively!
- source_path = content
- raise "Source #{source_path} is not a directory!" unless ::File.directory?(source_path)
+ src = source
+ raise "Source #{src} is not a directory!" unless ::File.directory?(src)
if ::File.exist?(@file_path)
raise "Destination #{@file_path} is not a directory!" unless ::File.directory?(@file_path)
- backup_recursively!(source_path, @file_path) unless @without_backup
+ backup_recursively!(src, @file_path) unless @without_backup
end
- do? "Copying #{source_path} -> #{@file_path} recursively" do
+ do? "Copying #{src} -> #{@file_path} recursively" do
if ::File.directory?(@file_path)
- Dir["#{source_path}/*"].each { FileUtils.cp_r(_1, @file_path) }
+ Dir["#{src}/*"].each { FileUtils.cp_r(_1, @file_path) }
else
- FileUtils.cp_r(source_path, @file_path)
+ FileUtils.cp_r(src, @file_path)
end
end
end
@@ -96,7 +104,9 @@ module RCM
return unless @conds_met
d = Directory.new(file_path)
- d.content(d.instance_eval(&block))
+ # Use source= for the recursive-copy source path rather than content=,
+ # keeping Directory's interface clean and purpose-named.
+ d.source(d.instance_eval(&block))
self << d
d
end
diff --git a/lib/dslkeywords/file.rb b/lib/dslkeywords/file.rb
index 273968c..dc6f4d0 100644
--- a/lib/dslkeywords/file.rb
+++ b/lib/dslkeywords/file.rb
@@ -9,12 +9,18 @@ require_relative 'file_backup'
module RCM
# Base class shared by all file-system resources (files, symlinks,
# touch, directories). Manages path, state (:present/:absent/:purged),
- # permissions (mode/owner/group), parent-directory lifecycle, and the
- # FileBackup mixin.
+ # permissions (mode/owner/group), and parent-directory lifecycle.
+ # Does NOT include content/templating — those belong in BaseFile so
+ # Touch and Directory (which have no file content) don't inherit them.
class BasicFile < Resource
include Chained
include FileBackup
+ # Raised by validate when an unsupported DSL option is used.
+ # Defined here so BasicFile#validate can raise it even when the
+ # concrete class does not extend BaseFile.
+ class UnsupportedOperation < StandardError; end
+
def initialize(file_path)
super(file_path)
@file_path = file_path
@@ -37,14 +43,6 @@ module RCM
true
end
- def content(text = nil)
- if text.nil?
- text = @from == :sourcefile ? ::File.read(@content) : @content
- return @from == :template ? ERB.new(text).result : text
- end
- @content = text.instance_of?(Array) ? text.join("\n") : text
- end
-
protected
def permissions!(file_path = path)
@@ -63,6 +61,18 @@ module RCM
"Unsupported '#{method}' operation #{what} (#{what.class})"
end
+ # Delete the resource and optionally remove orphaned parent directories.
+ # Used by File, Symlink, and Touch; Directory overrides this.
+ def evaluate_absent!
+ if ::File.exist?(@file_path)
+ do? "Deleting #{@file_path}" do
+ backup!(@file_path)
+ ::File.delete(@file_path) if ::File.file?(@file_path)
+ end
+ end
+ cleanup_parent_directory! if @manage_directory
+ end
+
def create_parent_directory!
dirname = ::File.dirname(@file_path)
return if ::File.directory?(dirname)
@@ -109,23 +119,22 @@ module RCM
end
end
- # Intermediate base for resources that have file content and support
- # :sourcefile / :template sourcing, and absent-state deletion.
+ # Intermediate base for resources that carry file content: regular files
+ # and symlinks. Adds content storage with optional ERB templating or
+ # sourcefile reading. Touch and Directory extend BasicFile directly so
+ # they are not burdened with content/from (ISP).
class BaseFile < BasicFile
- class UnsupportedOperation < StandardError; end
-
def from(what) = @from = validate(__method__, what.to_sym, :sourcefile, :template)
- protected
-
- def evaluate_absent!
- if ::File.exist?(@file_path)
- do? "Deleting #{@file_path}" do
- backup!(@file_path)
- ::File.delete(@file_path) if ::File.file?(@file_path)
- end
+ # Return or set the resource's content.
+ # Getter: resolves ERB templates or reads sourcefile on demand.
+ # Setter: stores plain text or joins an array with newlines.
+ def content(text = nil)
+ if text.nil?
+ text = @from == :sourcefile ? ::File.read(@content) : @content
+ return @from == :template ? ERB.new(text).result : text
end
- cleanup_parent_directory! if @manage_directory
+ @content = text.instance_of?(Array) ? text.join("\n") : text
end
end
diff --git a/lib/dslkeywords/touch.rb b/lib/dslkeywords/touch.rb
index d1073f2..27e691b 100644
--- a/lib/dslkeywords/touch.rb
+++ b/lib/dslkeywords/touch.rb
@@ -5,7 +5,9 @@ require_relative 'file'
module RCM
# Creates an empty file (touch semantics). Supports the additional
# :updated state which re-touches the file even when it already exists.
- class Touch < BaseFile
+ # Extends BasicFile directly — Touch has no file content or sourcing,
+ # so it must not inherit content/from from BaseFile (ISP).
+ class Touch < BasicFile
def is(what) = @is = validate(__method__, what.to_sym, :present, :absent, :purged, :updated)
def evaluate!