lib/hx.rb in hx-0.6.1 vs lib/hx.rb in hx-0.7.0

- old
+ new

@@ -20,10 +20,11 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. require 'rubygems' +require 'thread' require 'set' require 'pathname' require 'yaml' module Hx @@ -34,53 +35,76 @@ end class EditingNotSupportedError < RuntimeError end -module Source +module Filter def edit_entry(path, prototype=nil) raise EditingNotSupportedError, "Editing not supported for #{path}" end + def each_entry_path + each_entry { |path, entry| yield path } + end + def each_entry - raise NotImplementedError, "#{self.class}#each_entry not implemented" + each_entry_path do |path| + begin + entry = get_entry(path) + rescue NoSuchEntryError + next # entries may come and go during the enumeration + end + yield path, entry + end end + + def get_entry(path) + each_entry do |entry_path, entry| + return entry if entry_path == path + end + raise NoSuchEntryError, path + end end -class NullSource - include Source +class NullInput + include Filter def each_entry self end end -NULL_SOURCE = NullSource.new +NULL_INPUT = NullInput.new class PathSubset - include Source + include Filter - def initialize(source, options) - @source = source + def initialize(input, options) + @input = input @path_filter = Predicate.new(options[:only], options[:except]) end def edit_entry(path, prototype=nil) if @path_filter.accept? path - @source.edit_entry(path, prototype) { |text| yield text } + @input.edit_entry(path, prototype) { |text| yield text } else raise EditingNotSupportedError, "Editing not supported for #{path}" end self end - def each_entry - @source.each_entry do |path, entry| - yield path, entry if @path_filter.accept? path + def each_entry_path + @input.each_entry_path do |path| + yield path if @path_filter.accept? path end self end + + def get_entry(path) + raise NoSuchEntryError, path unless @path_filter.accept? path + @input.get_entry(path) + end end class PathSubset::Predicate def initialize(accept, reject) @accept_re = patterns_to_re(accept) @@ -113,33 +137,43 @@ end private :pattern_to_re end class Overlay - include Source + include Filter - def initialize(*sources) - @sources = sources + def initialize(*inputs) + @inputs = inputs end - def each_entry + def each_entry_path seen = Set[] - @sources.each do |source| - source.each_entry do |path, entry| - yield path, entry unless seen.include? path + @inputs.each do |input| + input.each_entry_path do |path| + yield path unless seen.include? path seen.add path end end self end + + def get_entry(path) + @inputs.each do |input| + begin + return input.get_entry(path) + rescue NoSuchEntryError + end + end + raise NoSuchEntryError, path + end end module CircumfixPath - include Source + include Filter - def initialize(source, options) - @source = source + def initialize(input, options) + @input = input @prefix = options[:prefix] @suffix = options[:suffix] prefix = Regexp.quote(@prefix.to_s) suffix = Regexp.quote(@suffix.to_s) @regexp = Regexp.new("^#{prefix}(.*)#{suffix}$") @@ -159,85 +193,114 @@ include CircumfixPath def edit_entry(path, prototype=nil) path = strip_circumfix(path) raise EditingNotSupportedError, "Editing not supported for #{path}" unless path - @source.edit_entry(path, prototype) { |text| yield text } + @input.edit_entry(path, prototype) { |text| yield text } self end - def each_entry - @source.each_entry do |path, entry| - yield add_circumfix(path), entry - end + def each_entry_path + @input.each_entry_path { |path| yield add_circumfix(path) } self end + + def get_entry(path) + path = strip_circumfix(path) + raise NoSuchEntryError, path unless path + @input.get_entry(path) + end end class StripPath include CircumfixPath def edit_entry(path, prototype=nil) path = add_circumfix(path) - @source.edit_entry(path, prototype) { |text| yield text } + @input.edit_entry(path, prototype) { |text| yield text } self end - def each_entry - @source.each_entry do |path, entry| + def each_entry_path + @input.each_entry_path do |path| path = strip_circumfix(path) - yield path, entry if path + yield path if path end self end + + def get_entry(path) + @input.get_entry(add_circumfix(path)) + end end class Cache - include Source + include Filter - def initialize(source, options={}) - @source = source + def initialize(input, options={}) + @input = input + @lock = Mutex.new @entries = nil + @entries_by_path = {} end def edit_entry(path, prototype=nil) - @source.edit_entry(path, prototype) { |text| yield text } + @input.edit_entry(path, prototype) { |text| yield text } self end def each_entry - unless @entries - entries = [] - @source.each_entry do |path, entry| - entries << [path, entry] + entries = nil + @lock.synchronize do + if @entries + entries = @entries + else + entries = [] + @input.each_entry do |path, entry| + @entries_by_path[path] = entry + entries << [path, entry] + end + @entries = entries end - @entries = entries end - @entries.each do |path, entry| + entries.each do |path, entry| yield path, entry.dup end self end + + def get_entry(path) + entry = nil + @lock.synchronize do + if @entries_by_path.has_key? path + entry = @entries_by_path[path] + else + entry = @input.get_entry(path) + @entries_by_path[path] = entry + end + end + return entry.dup + end end class Sort - include Source + include Filter - def initialize(source, options) - @source = source + def initialize(input, options) + @input = input @key_fields = Array(options[:sort_by] || []).map { |f| f.to_s } @reverse = !!options[:reverse] end def edit_entry(path, prototype=nil) - @source.edit_entry(path, prototype) { |text| yield text } + @input.edit_entry(path, prototype) { |text| yield text } self end def each_entry entries = [] - @source.each_entry do |path, entry| + @input.each_entry do |path, entry| entries << [path, entry] end unless @key_fields.empty? entries = entries.sort_by do |path, entry| @key_fields.map { |f| entry[f] } @@ -247,21 +310,25 @@ entries.each do |path, entry| yield path, entry end self end + + def get_entry(path) + @input.get_entry(path) + end end Chain = Object.new -def Chain.new(source, options) +def Chain.new(input, options) filters = options[:chain] || [] options = options.dup options.delete(:chain) # prevent inheritance for raw_filter in filters - source = Hx.build_source(options, source, {}, raw_filter) + input = Hx.build_source(options, input, {}, raw_filter) end - source + input end def self.make_default_title(options, path) name = path.split('/').last words = name.split(/[_\s-]/) @@ -284,12 +351,19 @@ def self.local_require(options, library) saved_require_path = $:.dup begin $:.delete(".") - $:.push Hx.get_pathname(options, :lib_dir).to_s + lib_dir = Hx.get_pathname(options, :lib_dir) + if lib_dir.relative? + $:.push "./#{lib_dir}" + else + $:.push lib_dir.to_s + end require library + rescue LoadError + raise ensure $:[0..-1] = saved_require_path end end @@ -299,36 +373,36 @@ rescue NameError raise NameError, "Unable to resolve #{qualified_name}" end end -def self.expand_chain(raw_source) - case raw_source +def self.expand_chain(raw_input) + case raw_input when Array # rewrite array to Hx::Chain - return NULL_SOURCE if raw_source.empty? + return NULL_INPUT if raw_input.empty? - filter_defs = raw_source.dup + filter_defs = raw_input.dup first_filter = filter_defs[0] = filter_defs[0].dup - raw_source = { + raw_input = { 'filter' => 'Hx::Chain', 'options' => {'chain' => filter_defs} } - if first_filter.has_key? 'source' # use input of first filter for chain - raw_source['source'] = first_filter['source'] - first_filter.delete('source') + if first_filter.has_key? 'input' # use input of first filter for chain + raw_input['input'] = first_filter['input'] + first_filter.delete('input') end end - raw_source + raw_input end def self.build_source(options, default_input, sources, raw_source) raw_source = expand_chain(raw_source) - if raw_source.has_key? 'source' - input_name = raw_source['source'] + if raw_source.has_key? 'input' + input_name = raw_source['input'] begin source = sources.fetch(input_name) rescue IndexError raise NameError, "No source named #{input_name} in scope" end @@ -381,19 +455,25 @@ source end class Site - include Source + include Filter attr_reader :options attr_reader :sources attr_reader :outputs class << self private :new + def load_file(config_file) + File.open(config_file, 'r') do |stream| + load(stream, config_file) + end + end + def load(io, config_path) raw_config = YAML.load(io) options = {} options[:base_dir] = File.dirname(config_path) for key, value in raw_config.fetch('options', {}) @@ -407,20 +487,20 @@ end raw_sources_by_name = raw_config.fetch('sources', {}) source_names = raw_sources_by_name.keys - # build source dependency graph + # build input dependency graph source_dependencies = {} for name, raw_source in raw_sources_by_name raw_source = Hx.expand_chain(raw_source) - if raw_source.has_key? 'source' - source_dependencies[name] = raw_source['source'] + if raw_source.has_key? 'input' + source_dependencies[name] = raw_source['input'] end end - # calculate depth for each source in the graph + # calculate depth for each input in the graph source_depths = Hash.new(0) for name in source_names seen = Set[] # for cycle detection while source_dependencies.has_key? name if seen.include? name @@ -437,17 +517,17 @@ depth_first_names = source_names.sort_by { |n| -source_depths[n] } sources = {} for name in depth_first_names raw_source = raw_sources_by_name[name] - sources[name] = Hx.build_source(options, NULL_SOURCE, sources, + sources[name] = Hx.build_source(options, NULL_INPUT, sources, raw_source) end outputs = [] for raw_output in raw_config.fetch('outputs', []) - outputs << Hx.build_source(options, NULL_SOURCE, sources, raw_output) + outputs << Hx.build_source(options, NULL_INPUT, sources, raw_output) end new(options, sources, outputs) end end @@ -462,55 +542,48 @@ def edit_entry(path, prototype=nil) @combined_output.edit_entry(path, prototype) { |text| yield text } self end - def each_entry - @combined_output.each_entry do |path, entry| - yield path, entry - end + def each_entry_path + @combined_output.each_entry_path { |path| yield path } self end -end -class FileBuilder - def initialize(output_dir) - @output_dir = Pathname.new(output_dir) + def get_entry(path) + @combined_output.get_entry(path) end +end - def build_file(path, entry) - build_file_helper(path, entry, false) +def self.refresh_file(pathname, content, update_time) + begin + return false if update_time and update_time < pathname.mtime + rescue Errno::ENOENT end + write_file(pathname, content) + true +end - def build_file_if_updated(path, entry) - build_file_helper(path, entry, true) - end - - def build_file_helper(path, entry, update_only) - filename = @output_dir + path - return self if update_only and filename.exist? and \ - entry['updated'] and filename.mtime >= entry['updated'] - dirname = filename.parent - dirname.mkpath() - filename.open("wb") do |stream| - stream.write entry['content'].to_s - end - self - end - private :build_file_helper +def self.write_file(pathname, content) + pathname.parent.mkpath() + pathname.open("wb") { |stream| stream << content.to_s } + nil end class LazyContent def initialize(&block) raise ArgumentError, "No block given" unless block + @lock = Mutex.new @content = nil @block = block end def to_s - if @block - @content = @block.call - @block = nil + @lock.synchronize do + if @block + @content = @block.call + @block = nil + end end @content end def to_yaml(*args)