require 'uri' module Couch class DesignDocument EXT_MIME_MAPPING = { ".html" => "text/html", ".js" => "text/javascript", ".css" => "text/css", } # initialize with a hash def initialize(hash = {}) @hash = hash end # returns the id of the design document def id @hash["_id"] end # returns the rev of the design document def rev @hash["_rev"] end # converts to json def to_json hash_with_injected_makros.to_json end # write to filesystem def write(&block) write_doc @hash, nil, block end class << self # returns new design document object from filesystem def build_from_filesystem(root) new map(root) end # returns new design document object from json string def build_from_json(json) new reject_makros(JSON.parse(json)) end # returns the database used to store design document def database @database ||= Couch.database end # returns the id for design document def id @id ||= File.read(File.join(Couch.root, '_id.js')).strip end # returns the url for design document def url(options = {}) File.join(database, id) << build_options_string(options) end private def reject_makros(doc) # TODO: recursive walk libs libs = doc["lib"] return doc if libs.nil? || libs.empty? # Attention: replace json makros first! doc = reject_json_makros(doc, libs) doc = reject_code_makros(doc, libs) end def reject_code_makros(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 substitute strings next unless content.is_a?(String) next unless value.include?(content.strip) doc[key] = value.gsub(content.strip, "// !code #{name}.js") end elsif value.is_a?(Hash) doc[key] = reject_code_makros(value, libs) end end doc end def reject_json_makros(doc, libs) doc.each do |key, value| next if key == "lib" if value.is_a?(String) libs.each do |name, content| # only substitute 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_makros(value, libs) end end doc 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 map(dirname, hash = {}) Dir.entries(dirname).each do |file| next if file =~ /^\./ filename = File.join(dirname, file) if file == "_attachments" hash['_attachments'] = map_attachments(filename) elsif File.directory?(filename) hash[file] = map(filename) elsif File.extname(filename) =~ /^\.(js|html)$/ # only .js and .html files are mapped # but .js is stripped off the key key = file.sub(/\.js$/, '') hash[key] = File.read(filename).strip end end hash end def map_attachments(dirname, hash = {}, keys = []) Dir.entries(dirname).each do |file| next if file =~ /^\./ filename = File.join(dirname, file) base = keys + [file] if File.directory?(filename) map_attachments filename, hash, base else hash[base.join('/')] = { "content_type" => mime_type(filename), "data" => base64(File.read(filename)), } end end hash end # CouchDB needs base64 encodings without spaces def base64(data) [data].pack("m").gsub(/\s/,'') end # detect mime type from filename extension def mime_type(filename) ext = File.extname(filename) EXT_MIME_MAPPING[ext] || 'text/plain' end end private def write_doc(doc, dirname, block) doc.each do |key, value| next if key == "_attachments" filename = dirname ? File.join(dirname, key) : key.dup if value.is_a?(String) filename << ".js" unless File.extname(filename) == ".html" block.call filename, "#{value}\n" else write_doc value, filename, block end end write_attachments doc["_attachments"], dirname, block if doc["_attachments"] end def write_attachments(doc, dirname, block) dirname = dirname ? File.join(dirname, "_attachments") : "_attachments" doc.each do |key, value| next unless value["data"] block.call File.join(dirname, key), value["data"].unpack("m") end end def hash_with_injected_makros hash = inject_code_makros(@hash) inject_json_makros(hash) end def inject_code_makros(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') File.read(File.join(Couch.root, 'lib', filename)).strip end elsif value.is_a?(Hash) inject_code_makros(value) else value end end doc end def inject_json_makros(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(/\..*$/, ''), File.read(File.join(Couch.root, 'lib', filename)).strip.to_json] end elsif value.is_a?(Hash) inject_json_makros(value) else value end end doc end end end