Class: OKF::Bundle::Reader

Inherits:
Object
  • Object
show all
Defined in:
lib/okf/bundle/reader.rb

Overview

Reads an OKF bundle directory into an in-memory OKF::Bundle. Together with Bundle::Writer this is the only component that touches the filesystem — the core (Bundle, Concept, Graph, Validator, Linter) then works purely in memory.

It parses eagerly: each concept file becomes an OKF::Concept, each index.md/log.md is kept as raw text (its structure is validated as text), and a file the reader cannot use — frontmatter that does not parse, or a file it cannot open at all — is retained as an unparseable entry (carrying the ParseError message or the errno, so §9.1 can report it) rather than dropped or raised. That tolerance is the whole §9 best-effort promise: one bad file never breaks the rest, and this is the read every verb shares. Every read goes through Path.join_under! so a symlinked or crafted path cannot escape the bundle root — that guard still raises, because a path leaving the root is not a bad file, it is a bundle lying about its shape.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(dir) ⇒ Reader

Returns a new instance of Reader.



26
27
28
# File 'lib/okf/bundle/reader.rb', line 26

def initialize(dir)
  @root = File.expand_path(dir.to_s)
end

Instance Attribute Details

#rootObject (readonly)

Returns the value of attribute root.



24
25
26
# File 'lib/okf/bundle/reader.rb', line 24

def root
  @root
end

Class Method Details

.read(dir) ⇒ Object



20
21
22
# File 'lib/okf/bundle/reader.rb', line 20

def self.read(dir)
  new(dir).read
end

Instance Method Details

#readObject



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/okf/bundle/reader.rb', line 30

def read
  concepts = []
  reserved = []
  unparseable = []

  markdown_paths.each do |path|
    begin
      content = File.read(Path.join_under!(@root, path), encoding: "UTF-8")
      if Concept.reserved?(path)
        reserved << Entry.new(path: path, content: content)
      else
        frontmatter, body = Markdown::Frontmatter.parse(content)
        concepts << Concept.new(path: path, frontmatter: frontmatter, body: body)
      end
    rescue Markdown::Frontmatter::ParseError => e
      unparseable << Entry.new(path: path, content: content, error: e.message)
    rescue SystemCallError => e
      # A file that cannot be opened is one unusable file, not a broken
      # bundle. Letting the errno out of here breaks "one bad file never
      # breaks the rest" for every verb at once — the read is the one path
      # they all share — and it breaks it in the worst way: a backtrace,
      # under an exit code that claims the bundle is non-conformant. So it
      # joins the same bucket a bad frontmatter block does, and §9.1 reports
      # it naming the file and the errno.
      #
      # Its content is "" rather than nil: unknown, but every analyzer reads
      # it as text, and empty is the honest shape of a file we never saw —
      # no links to resolve, no encoding to be invalid, nothing claimed.
      unparseable << Entry.new(path: path, content: "", error: e.message)
    end
  end

  Bundle.new(concepts: concepts, reserved: reserved, unparseable: unparseable, root: @root)
end