Class: OKF::Concept::File

Inherits:
Object
  • Object
show all
Defined in:
lib/okf/concept/file.rb

Overview

A single concept file on disk — the ActiveRecord-style handle over one .md. Wraps a pure OKF::Concept with load/save/delete/reload side effects. Part of the shell (it does I/O); the pure Concept knows nothing about it.

file = OKF::Concept::File.read(root: "docs", path: "tables/orders.md")
file.concept              # => OKF::Concept (pure)
file.concept.links        # interrogate it in memory
file.save                 # write concept.to_markdown back to disk
file.delete

NOTE: this class is named File, which shadows Ruby's File inside the OKF::Concept namespace — every filesystem call here uses ::File explicitly.

absolute_path guards the name lexically (Path.join_under!), which is all a write needs — the file may not exist yet. A read has more to prove: the file is on disk now, so it may be a symlink whose name is inside the root but whose target is not, and File.expand_path does not resolve links. So #read goes through SafeRead, which realpath-resolves and refuses a target outside the root, closing the same escape Bundle::Reader closes on the bulk read.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root:, path:, concept: nil) ⇒ File

Returns a new instance of File.



37
38
39
40
41
# File 'lib/okf/concept/file.rb', line 37

def initialize(root:, path:, concept: nil)
  @root = ::File.expand_path(root.to_s)
  @path = Path.normalize_relative!(path)
  @concept = concept
end

Instance Attribute Details

#conceptObject (readonly)

Returns the value of attribute concept.



25
26
27
# File 'lib/okf/concept/file.rb', line 25

def concept
  @concept
end

#pathObject (readonly)

Returns the value of attribute path.



25
26
27
# File 'lib/okf/concept/file.rb', line 25

def path
  @path
end

#rootObject (readonly)

Returns the value of attribute root.



25
26
27
# File 'lib/okf/concept/file.rb', line 25

def root
  @root
end

Class Method Details

.read(root:, path:) ⇒ Object

Read a concept file from disk into a handle.



28
29
30
# File 'lib/okf/concept/file.rb', line 28

def self.read(root:, path:)
  new(root: root, path: path).reload
end

.write(root:, concept:) ⇒ Object

Write a concept's markdown to disk under root and return the handle.



33
34
35
# File 'lib/okf/concept/file.rb', line 33

def self.write(root:, concept:)
  new(root: root, path: concept.path, concept: concept).save
end

Instance Method Details

#absolute_pathObject

Absolute on-disk path, guarded so it cannot escape the bundle root.



44
45
46
# File 'lib/okf/concept/file.rb', line 44

def absolute_path
  Path.join_under!(@root, @path)
end

#deleteObject



57
58
59
60
# File 'lib/okf/concept/file.rb', line 57

def delete
  ::FileUtils.rm_f(absolute_path)
  self
end

#readObject

The file's own bytes, refused if the resolved target escapes the root by symlink. This is the guarded read a caller that wants the raw markdown (not a re-serialized concept.to_markdown) must use instead of reading #absolute_path itself — that path guards the name lexically, which a write needs but a read does not, since the file exists and may be a link.



73
74
75
# File 'lib/okf/concept/file.rb', line 73

def read
  SafeRead.read!(@root, absolute_path)
end

#reloadObject



62
63
64
65
66
# File 'lib/okf/concept/file.rb', line 62

def reload
  frontmatter, body = Markdown::Frontmatter.parse(read)
  @concept = Concept.new(path: @path, frontmatter: frontmatter, body: body)
  self
end

#saveObject

Raises:



48
49
50
51
52
53
54
55
# File 'lib/okf/concept/file.rb', line 48

def save
  raise Error, "no concept to save" if @concept.nil?

  target = absolute_path
  ::FileUtils.mkdir_p(::File.dirname(target))
  ::File.write(target, @concept.to_markdown, encoding: "UTF-8")
  self
end