Class: Scarpe::Components::SegmentedFileLoader

Inherits:
Object
  • Object
show all
Includes:
FileHelpers
Defined in:
scarpe-components/lib/scarpe/components/segmented_file_loader.rb

Class Method Summary collapse

Instance Method Summary collapse

Methods included from FileHelpers

#with_tempfile, #with_tempfiles

Class Method Details

.front_matter_and_segments_from_file(contents) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'scarpe-components/lib/scarpe/components/segmented_file_loader.rb', line 75

def self.front_matter_and_segments_from_file(contents)
  require "yaml" # Only load when needed
  require "English"

  segments = contents.split(/\n-{5,}/)
  front_matter = {}

  # The very first segment can start with front matter, or with a divider, or with no divider.
  if segments[0].start_with?("---\n") || segments[0] == "---"
    # We have YAML front matter at the start. All later segments will have a divider.
    front_matter = YAML.load segments[0]
    front_matter ||= {} # If the front matter is just the three dashes it returns nil
    segments = segments[1..-1]
  elsif segments[0].start_with?("-----")
    # We have a divider at the start. Great! We're already well set up for this case.
  elsif segments.size == 1
    # No front matter, no divider, a single unnamed segment. No more parsing needed.
    return [{}, { "" => segments[0] }]
  else
    # No front matter, no divider before the first segment, multiple segments.
    # We'll add an artificial divider to the first segment for uniformity.
    segments = ["-----\n" + segments[0]] + segments[1..-1]
  end

  segmap = {}
  segments.each do |segment|
    if segment =~ /\A-* +(.*?)\n/
      # named segment with separator
      name = ::Regexp.last_match(1)

      raise("Duplicate segment name: #{name.inspect}!") if segmap.key?(name)

      segmap[name] = ::Regexp.last_match.post_match
    elsif segment =~ /\A-* *\n/
      # unnamed segment with separator
      segmap[gen_name(segmap)] = ::Regexp.last_match.post_match
    else
      raise Scarpe::InternalError, "Internal error when parsing segments in segmented app file! seg: #{segment.inspect}"
    end
  end

  [front_matter, segmap]
end

.gen_name(segmap) ⇒ Object



70
71
72
73
# File 'scarpe-components/lib/scarpe/components/segmented_file_loader.rb', line 70

def self.gen_name(segmap)
  ctr = (1..10_000).detect { |i| !segmap.key?("%5d" % i) }
  "%5d" % ctr
end

Instance Method Details

#add_segment_type(type, handler) ⇒ void

This method returns an undefined value.

Add a new segment type (e.g. "catscradle") with a different file handler.

Parameters:

  • type (String)

    the new name for this segment type

  • handler (Object)

    an object that will be called as obj.call(filename) - often a proc



15
16
17
18
19
20
21
# File 'scarpe-components/lib/scarpe/components/segmented_file_loader.rb', line 15

def add_segment_type(type, handler)
  if segment_type_hash.key?(type)
    raise Shoes::Errors::InvalidAttributeValueError, "Segment type #{type.inspect} already exists!"
  end

  segment_type_hash[type] = handler
end

#after_load(&block) ⇒ Object

Segment type handlers can call this to perform an operation after the load has completed. This is important for ordering, and because loading a Shoes app often doesn't return. So to have a later section (e.g. tests, additional data) do something that affects Shoes app loading (e.g. set an env var, affect the display service) it's important that app loading take place later in the sequence.



65
66
67
68
# File 'scarpe-components/lib/scarpe/components/segmented_file_loader.rb', line 65

def after_load(&block)
  @after_load ||= []
  @after_load << block
end

#call(path) ⇒ Boolean

Load a .sca file with an optional YAML frontmatter prefix and multiple file sections which can be treated differently.

The file loader acts like a proc, being called with .call() and returning true or false for whether it has handled the file load. This allows chaining loaders in order and the first loader to recognise a file will run it.

Parameters:

  • path (String)

    the file or directory to treat as a Scarpe app

Returns:

  • (Boolean)

    return true if the file is loaded as a segmented Scarpe app file



52
53
54
55
56
57
# File 'scarpe-components/lib/scarpe/components/segmented_file_loader.rb', line 52

def call(path)
  return false unless path.end_with?(".scas") || path.end_with?(".sspec")

  file_load(path)
  true
end

#file_load(path) ⇒ Object



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'scarpe-components/lib/scarpe/components/segmented_file_loader.rb', line 119

def file_load(path)
  contents = File.read(path)

  front_matter, segmap = self.class.front_matter_and_segments_from_file(contents)

  if segmap.empty?
    raise Scarpe::FileContentError, "Illegal segmented Scarpe file: must have at least one code segment, not just front matter!"
  end

  if front_matter[:segments]
    if front_matter[:segments].size != segmap.size
      raise Scarpe::FileContentError, "Number of front matter :segments must equal number of file segments!"
    end
  else
    if segmap.size > 2
      raise Scarpe::FileContentError, "Segmented files with more than two segments have to specify what they're for!"
    end

    # Set to default of shoes code only or shoes code and app test code.
    front_matter[:segments] = segmap.size == 2 ? ["shoes", "app_test"] : ["shoes"]
  end

  # Match up front_matter[:segments] with the segments, or use the default of shoes and app_test.

  sth = segment_type_hash
  sv = segmap.values

  tf_specs = []
  front_matter[:segments].each.with_index do |seg_type, idx|
    unless sth.key?(seg_type)
      raise Scarpe::FileContentError, "Unrecognized segment type #{seg_type.inspect}! No matching segment type available!"
    end

    tf_specs << ["scarpe_#{seg_type}_segment_contents", sv[idx]]
  end

  with_tempfiles(tf_specs) do |filenames|
    filenames.each.with_index do |filename, idx|
      seg_name = front_matter[:segments][idx]
      sth[seg_name].call(filename)
    end

    # Need to call @after_load hooks while tempfiles still exist
    if @after_load && !@after_load.empty?
      @after_load.each(&:call)
      @after_load = []
    end
  end
end

#remove_all_segment_types!void

This method returns an undefined value.

Normally a Shoes application will want to keep the default segment types, which allow loading a Shoes app and running a test inside. But sometimes the default handler will be wrong and a library will want to register its own "shoes" and "app_test" segment handlers, or not have any at all. For those applications, it makes sense to clear all segment types before registering its own.



38
39
40
# File 'scarpe-components/lib/scarpe/components/segmented_file_loader.rb', line 38

def remove_all_segment_types!
  @segment_type_hash = {}
end

#segment_type_hashHash<String, Object>

The hash of segment type labels mapped to handlers which will be called. This could be called by a display service, a test framework or similar code that wants to define a non-Scarpe-standard file format.

Returns:

  • (Hash<String, Object>)

    the name/handler pairs



174
175
176
177
178
179
180
181
182
# File 'scarpe-components/lib/scarpe/components/segmented_file_loader.rb', line 174

def segment_type_hash
  @segment_handlers ||= {
    "shoes" => proc { |seg_file| after_load { load seg_file } },
    "app_test" => proc do |seg_file|
      ENV["SHOES_SPEC_TEST"] = seg_file
      ENV["SHOES_MINITEST_EXPORT_FILE"] ||= "sspec.json"
    end,
  }
end

#segment_typesArray<String>

Return an Array of segment type labels, such as "code" and "app_test".

Returns:

  • (Array<String>)

    the segment type labels



26
27
28
# File 'scarpe-components/lib/scarpe/components/segmented_file_loader.rb', line 26

def segment_types
  segment_type_hash.keys
end