require 'uri' module Couch class DesignDocument # Mime type mapping from extensions MIME_TYPE_MAPPING = { ".html" => "text/html", ".js" => "text/javascript", ".css" => "text/css", } # Files that should have a .js extension JAVASCRIPT_FILES = %w[ validate_doc_update lists/* shows/* updates/* views/*/* ] # Files that should not be included in document EXCLUDE_FILES = %w[ README ] attr_accessor :hash def initialize @hash = {} end # Read document from a filesystem. # # Takes a filename, # many filenames, # or an array of filenames # and assign the return value of a yielded block to the hash. # # Nested hashes # like { "hash" => { "key" => "value" } } # can be constructed if the filename contains a slash (/), # eg "hash/key". # def read(*filenames, &block) filenames.flatten.uniq.each do |filename| # skip exclude files next if EXCLUDE_FILES.include?(filename) key = filename.dup # strip extname from javascript files key.sub!(/\.js$/, '') if filename =~ /#{JAVASCRIPT_FILES.join('|')}/ set_hash_at key, block.call(filename) end map_attachments! inject_makros! end # Write document to a filesystem # # Takes a directoy as startpoint (default is nil), # a document hash (default is the design documents hash) # and recursively yields all keys and values to the given block. # # Nested hashes # like { "hash" => { "key" => "value" } } # will result in the yielded filename # "hash/key". # # The key "_attachments" has a special meaning: # the value holds base64 encoded data as well as other metadata. # This data will gets decoded and used as value for the key. # def write(directory = nil, doc = nil, &block) reduce_attachments! reject_makros! doc ||= hash doc.each do |key, value| filename = directory ? File.join(directory, key) : key.dup if value.is_a?(Hash) write(filename, value, &block) else # append extname to javascript files filename << '.js' if filename =~ /#{JAVASCRIPT_FILES.join('|')}/ block.call(filename, value) end end end # Returns a JSON string representation of the documents hash # def json hash.to_json end # Build the documents hash from a JSON string # def json=(json) self.hash = JSON.parse(json) end # Accessor for id def id hash["_id"] || Couch.id end # Accessor for rev def rev hash["_rev"] || Couch.rev end # Updates rev in documents hash def rev=(new_rev) hash["_rev"] = new_rev end # Accessor for couch database def database @database ||= Couch.database end # Base URL for document def base_url @base_url ||= File.join(database, id) end # URL for accessing design document # # Takes an optional options hash # which gets converted to url encoded options # and appended to the documents base url # def url(options = {}) base_url + build_options_string(options) end private def hash_at(path) current_hash = hash parts = path.split('/') key = parts.pop parts.each do |part| current_hash[part] ||= {} current_hash = current_hash[part] end current_hash[key] end def set_hash_at(path, value) current_hash = hash parts = path.split('/') key = parts.pop parts.each do |part| current_hash[part] ||= {} current_hash = current_hash[part] end current_hash[key] = value end def build_options_string(options) return '' if options.empty? options_array = [] options.each do |key, value| options_array << URI.escape([key, value].join('=')) end '?' + options_array.join("&") end def inject_makros! self.hash = inject_code_makro(hash) self.hash = inject_json_makro(hash) end def inject_code_makro(doc) doc.each do |key, value| doc[key] = if value.is_a?(String) value.gsub(/\/\/\s*!code.*$/) do |match| filename = match.sub(/^.*!code\s*(\S+).*$/, '\1') hash_at File.join('lib', filename) end elsif value.is_a?(Hash) inject_code_makro(value) else value end end doc end def inject_json_makro(doc) doc.each do |key, value| doc[key] = if value.is_a?(String) value.gsub(/\/\/\s*!json.*$/) do |match| filename = match.sub(/^.*!json\s*(\S+).*$/, '\1') 'var %s = %s;' % [filename.sub(/\..*$/, ''), hash_at(File.join('lib', filename)).to_json] end elsif value.is_a?(Hash) inject_json_makro(value) else value end end doc end def reject_makros! # TODO: recursive walk libs libs = hash["lib"] return if libs.nil? || libs.empty? # Attention: replace json makros first! self.hash = reject_json_makro(hash, libs) self.hash = reject_code_makro(hash, libs) end def reject_code_makro(doc, libs) doc = doc.dup doc.each do |key, value| next if key == "lib" if value.is_a?(String) libs.each do |name, content| # only try substituting strings next unless content.is_a?(String) next unless value.include?(content) doc[key] = value.gsub(content, "// !code #{name}") end elsif value.is_a?(Hash) doc[key] = reject_code_makro(value, libs) end end doc end def reject_json_makro(doc, libs) doc.each do |key, value| next if key == "lib" if value.is_a?(String) libs.each do |name, content| # only try substituting strings next unless content.is_a?(String) json = 'var %s = %s;' % [name.sub(/\..*$/, ''), content.to_json] next unless value.include?(json) doc[key] = value.gsub(json, "// !json #{name}") end elsif value.is_a?(Hash) doc[key] = reject_json_makro(value, libs) end end doc end def reduce_attachments! return hash unless hash["_attachments"] attachments = {} hash["_attachments"].each do |key, value| data = value["data"] next unless data attachments.update key => decode_attachment(data) end hash.update "_attachments" => attachments end def map_attachments! return unless hash["_attachments"] attachments = {} flatten_attachements(hash["_attachments"]).each do |key, value| attachments.update key => { "data" => encode_attachment(value), "content_type" => mime_type_for(key) } end self.hash.update "_attachments" => attachments end def flatten_attachements(doc, base = nil) result = {} doc.each do |key, value| new_base = base ? [base, key].join('/') : key if value.is_a?(Hash) result.update flatten_attachements(value, new_base) else result.update new_base => value end end result end def decode_attachment(data) data.unpack("m").first end def encode_attachment(data) [data].pack("m").gsub(/\s+/,'') end def mime_type_for(filename) ext = File.extname(filename) MIME_TYPE_MAPPING[ext] || 'text/plain' end end end