module Topical class ParseError < StandardError; end class TopicTree Contents = Struct.new(:name, :description, :data) include Enumerable attr_reader :data def initialize @data = {} end def clear! @data = {} end def create(path, name=nil, description=nil, data=[]) path = path + "/" unless path[-1] == ?/ begin check_absolute_path path rescue ParseError raise ParseError, "Invalid topic to create: #{path}" end paths = [] path.split("/").each { |piece| paths << paths.last.to_s + piece + "/" } paths << path if path == "/" paths.each { |subpath| if subpath == paths.last unless @data[subpath].kind_of?(Contents) @data[subpath] = Contents.new(name, description, data) end @data[subpath].name = name else unless @data[subpath].kind_of?(Contents) @data[subpath] = Contents.new end end } end def describe(path, name, description=nil) each(path, false) { |contents| contents.name = name contents.description = description } end def add(path, object) added = false each(path, false) { |content| content.data << object added = true } raise "No matching topic found for #{path}." unless added object end def delete(object) @data.each { |k, v| v.data.delete object unless v.data.nil? } end def name(path) path = path + "/" unless path[-1] == ?/ check_absolute_path path if @data[path] @data[path].name else nil end end def description(path) path = path + "/" unless path[-1] == ?/ check_absolute_path path if @data[path] @data[path].description else nil end end def fetch(path) result = [] each(path) { |content| result.concat content.data unless content.data.nil? } result.uniq end def fetch_unified(path, *selectors) result = fetch path return result if selectors.empty? result.map { |e| if selectors.all? { |sel| fetch(sel).include? e } e else nil end }.compact end def [](*args) if args.size == 0 nil elsif args.size == 1 fetch args.first else fetch_unified(*args) end end def each(path, submatch=true, &block) each_path(path, submatch) { |key| block.call @data[key] } end def each_path(path, submatch=true, &block) path = self.class.path2regexp path, submatch @data.keys.grep(path).each { |key| block.call key } end def subtopics(root='/') r = self.class.path2regexp root, true s = [] each_path(root) { |p| s << p.gsub(r, '').gsub(/\/.*/, '') } s.delete "" s.uniq end def self.path2regexp(path, submatch=true) unless path =~ /\A[-\w\/*]*\Z/ and not path.index '//' \ and not path.index '***' raise ParseError, "Invalid topic: #{path}" end path = path + "/" unless path[-1] == ?/ regex = "" regex << "^" if path =~ /^\// regex << path.gsub(/((\*\*?\/)+)/) { piece = $1 if piece =~ /\*\*/ n = (piece.count('/')-piece.scan("**").length).to_s + "," else n = piece.count('/').to_s end "([^/]*/){#{n}}" } regex << "$" unless submatch Regexp.new(regex) end def check_absolute_path path unless path =~ %r{\A/([\w_-]+/)*\Z} raise ParseError, "Invalid absolute topic: #{path}" end end end end