coffeeapp.coffee | |
---|---|
CoffeeApp - coffee-script wrapper for CouchApp Copyright 2010 Andrzej Sliwa (andrzej.sliwa@i-tool.eu) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. | |
fast way to imports using pattern matching | {existsSync, join, extname} = require 'path'
{mkdirSync, readdirSync, writeFileSync, readFileSync, statSync, symlinkSync, unlinkSync} = require 'fs'
{compile} = require 'coffee-script'
{exec, spawn} = require 'child_process'
{print, gets} = require 'util'
{_} = require 'underscore'
{log} = console
request = require 'request'
headers =
accept: 'application/json',
'content-type': 'application/json'
dumps_folder_name = '.dumps'
releases_folder_name = '.releases' |
Command wrapping configuration. | commandWraps = [
{
name: 'push',
type: 'before',
callback: -> grindCoffee()
},
{
name: 'help',
type: 'after',
desc: 'show this message',
callback: -> help()
},
{
name: 'cgenerate',
type: 'before',
desc: '[ view | list | show | filter ] generate .coffee versions',
callback: -> generate()
},
{
name: 'destroy',
type: 'before',
desc: '[ view | list | show | filter ] destroy (remove directory/files also .js files).',
callback: -> destroy()
},
{
name: 'prepare',
type: 'before',
desc: 'prepare (.gitignore...)',
callback: -> prepare()
},
{
name: 'clean',
type: 'before',
desc: "remove #{releases_folder_name} & #{dumps_folder_name} directories"
callback: -> clean()
},
{
name: 'restore',
type: 'before',
desc: "restore database from #{dumps_folder_name}/last"
callback: -> restore()
}
] |
File templates | |
.gitignore | gitIgnore = """
.DS_Store
.couchapprc
#DIVIDER
""" |
{dumpsfoldername}/* {dumpsfoldername}//* {releasesfoldername}/* {releasesfoldername}//* | couchAppIgnore = '''
[
"spec",
"features"
]
''' |
.couchappignore | mapCoffee = '''
(doc) ->
...
''' |
map function | reduceCoffee = '''
(keys, values, rereduce) ->
...
''' |
reduce function | listCoffee = '''
(head, req) ->
...
''' |
list function | showCoffee = '''
(doc, req) ->
...
''' |
show function | filterCoffee = '''
(doc, req) ->
...
''' |
filter function | getVersion = ->
readFileSync(join(__dirname, '..', 'package.json')).toString().match(/"version"\s*:\s*"([\d.]+)"/)[1]
showGreatings = -> |
Helper Methods | log "CoffeeApp (#{getVersion()}) - simple coffee-script wrapper for CouchApp (http://couchapp.org)"
log 'http://github.com/andrzejsliwa/coffeeapp\n'
spaces = (string, max) ->
count = max - string.length
result = ""
while count > 0
result += " "
count--
result |
Shows greatings ... | padTwo = (number) ->
result = if number < 10 then '0' else ''
"#{result}#{number}" |
Zero padding for format '0x' | getTimestamp = ->
date = new Date
date.getFullYear() + padTwo(date.getMonth() + 1) +
padTwo(date.getDate()) + padTwo(date.getHours() + 1) +
padTwo(date.getMinutes() + 1) + padTwo(date.getSeconds() + 1) |
Timestamp string based on current date | handleOutput = (callbackOk, callbackError) ->
(error, stdout, stderr) ->
if error != null
if callbackError != undefined
callbackError()
else
log stderr if stderr && stderr.length > 0
log "exec error: #{error}"
else
log stdout if stdout && stdout.length > 0
if callbackOk != undefined
callbackOk()
getConfig = ->
JSON.parse readFileSync '.couchapprc', 'utf8'
getDirectories = (currentDir, ignores) ->
callback = (name) ->
statSync(join currentDir, name).isDirectory()
filterDirectory currentDir, callback, ignores
getFiles = (currentDir, ignores) ->
callback = (name) ->
!statSync(join currentDir, name).isDirectory()
filterDirectory currentDir, callback, ignores
filterDirectory = (currentDir, callback, ignores) ->
list = readdirSync currentDir
if ignores
list = _.without(list, ignores...)
results = _.filter list, callback |
Display outputs if presents | |
Main Methods | processDirectory = (baseDir, destination) ->
dirs = [baseDir]
isError = false
while (dirs.length > 0)
currentDir = dirs.pop()
subDirs = getDirectories currentDir, ['.git', releases_folder_name, dumps_folder_name]
_.each subDirs, (dirName) ->
dirPath = join currentDir, dirName
dirs.push dirPath
destDirPath = join destination, join dirPath
mkdirSync destDirPath, 0700
files = getFiles currentDir
_.each files, (fileName) ->
filePath = join currentDir, fileName
destFilePath = join destination, filePath
if extname(filePath) == ".coffee"
log " * processing #{filePath}..."
try
writeFileSync destFilePath.replace(/\.coffee$/, '.js'),
compile(readFileSync(filePath, 'utf8'), bare: yes), 'utf8'
catch error
log "Compilation Error: #{error.message}\n"
isError = true
else
writeFileSync destFilePath, readFileSync(filePath, 'binary'), 'binary'
!isError |
Process directory "recursivly", normal files are copied, directories are recreated and .coffee files are "compiled" to javascript | |
Grinding of coffee | grindCoffee = ->
log "Wrapping 'push' of couchapp"
timestamp = getTimestamp()
releasesDir = releases_folder_name
unless existsSync releasesDir
log "initialize #{releasesDir} directory"
mkdirSync releasesDir, 0700
releasePath = join releasesDir, timestamp
[options, database] = processOptions()
log "\ndatabase : '#{database}'\n"
processCallback = ->
log "preparing release: #{releasePath}"
mkdirSync releasePath, 0700
if processDirectory '.', releasePath
process.chdir releasePath
exec "couchapp push #{options} #{database}", handleOutput process.cwd
config = getConfig()
if config['env'][database]['make_dumps']
dumpsDir = join dumps_folder_name, database
unless existsSync dumps_folder_name
log "initialize #{dumps_folder_name} directory"
mkdirSync dumps_folder_name, 0700
unless existsSync dumpsDir
log "initialize #{dumpsDir} directory"
mkdirSync dumpsDir, 0700
dumpsPath = join dumpsDir, timestamp
log "making dump: #{dumpsPath}"
url = config['env'][database]['db']
exec "couchdb-dump #{url} > #{dumpsPath}", handleOutput ->
lastPath = join dumpsDir, 'last'
unlinkSync lastPath if existsSync lastPath
log " * linking dump: #{dumpsPath} -> #{lastPath}"
symlinkSync timestamp, "#{lastPath}"
processCallback()
else
processCallback()
processOptions = ->
[options..., database] = process.argv[2..]
options = (options || []).join ' '
database = "default" if database != null
return [options, database] |
Starts wrapping push command of couchapps each deploy is moved to .releases/[timestamp] directory with processing coffee-script files and then pushed from deploy directory. | help = ->
log "Wrapping 'help' of couchapp\n"
showGreatings()
usage = '''
Usage: coffeeapp [OPTIONS] [CMD] [CMDOPTIONS] [ARGS,...]
Commands:
'''
log usage
_.each commandWraps, (command) ->
if command.desc
log " #{command.name}#{spaces(command.name, 9)} [OPTIONS]..."
log " #{command.desc}\n" |
Shows available options. | generateFile = (path, template) ->
log " * creating #{path}..."
if existsSync path
log "File #{path} already exist!"
else
writeFileSync path, template, 'utf8' |
generate file from template verbosly | generate = -> operateOn('generate')
destroy = -> operateOn('destroy') |
cgenerate and destory handling | operateOn = (command) ->
generator = process.argv[2..][1]
unless generator
log "missing name of #{command} - [ view | list | show | filter ]"
return
name = process.argv[2..][2]
unless name
log 'missing name of element'
return
print "Running #{generator} #{command}:\n"
fun = switch generator
when 'view'
handleView
when 'show'
handleShow
when 'list'
handleList
when 'filter'
handleFilter
else
(method, name) -> log "unknown #{command}"
log 'done.' if fun(command, name) |
common handling for cgenerate/destroy | handleView = (method, name) ->
unless existsSync 'views'
mkdirSync 'views', 0700
viewDirPath = "views/#{name}"
[mapFilePath, reduceFilePath] = [join(viewDirPath, "map.coffee"), join(viewDirPath, "reduce.coffee")]
switch method
when 'generate'
if existsSync viewDirPath
log "directory '#{viewDirPath}' already exist!"
false
else
mkdirSync viewDirPath, 0700
generateFile mapFilePath, mapCoffee
generateFile reduceFilePath, reduceCoffee
true
when 'destroy'
if existsSync viewDirPath
log "'#{viewDirPath}'."
exec "rm -r #{viewDirPath}", handleOutput()
true
else
log "there is no view '#{name}' ('#{viewDirPath}') !!!"
false
else
throw 'unknown method' |
handling view generate/destroy | handleFile = (method, folder, template, name) ->
filePathCoffee = join folder, "#{name}.coffee"
filePathJS = join folder, "#{name}.js"
switch method
when 'generate'
unless existsSync folder
mkdirSync folder, 0700
generateFile filePathCoffee, template
true
when 'destroy'
if existsSync filePathCoffee
log "'#{filePathCoffee}'."
exec "rm #{filePathCoffee}", handleOutput()
true
else if existsSync filePathJS
log "'#{filePathJS}'."
exec "rm #{filePathJS}", handleOutput()
true
else
log "there is no '#{name}' ('#{filePathJS}' or '#{filePathCoffee}') !!!"
false
else
throw 'unknown method' |
handling generic generate/destroy of file | handleShow = (method, name) -> handleFile(method, 'shows', showCoffee, name)
handleList = (method, name) -> handleFile(method, 'lists', listCoffee, name)
handleFilter = (method, name) -> handleFile(method, 'filters', filterCoffee, name) |
shortcuts of handling generate/destroy | clean = ->
log "cleaning up:"
log " * remove '#{releases_folder_name}' ..."
exec "rm -r #{releases_folder_name}", handleOutput ->
log " * remove '#{dumps_folder_name}' ..."
exec "rm -r #{dumps_folder_name}", handleOutput ->
log "done."
, ->
log "there is no '#{dumps_folder_name}' directory!"
, ->
log "there is no '#{releases_folder_name}' directory!" |
make clean up | prepare = ->
log "preparing project:"
generateFile '.gitignore', gitIgnore
generateFile '.couchappignore', couchAppIgnore
log "done."
restore = ->
[options, database] = processOptions()
config = getConfig()
lastPath = join dumps_folder_name, database, 'last'
unless existsSync lastPath
log "There is no dump!"
return
if config['env'][database]['make_dumps']
log "restoring dump from #{lastPath} to database: #{}"
url = config['env'][database]['db']
request {uri: url, method: 'DELETE', headers: headers}, (error, response, body) ->
if response.statusCode == 200
request {uri: url, method: 'PUT', headers: headers}, (error, response, body) ->
unless response.statusCode == 200
message = JSON.parse(body)
log "Error: #{message.error} - #{message.reason}"
else
message = JSON.parse(body)
log "Error: #{message.error} - #{message.reason}"
exec "couchdb-load #{url} --input #{lastPath}", handleOutput ->
log "done."
else
log "you don't using dumps for this database... look in couchapprc for make_dumps." |
prepare project | handleCommand = (type) ->
handled = false
name = process.argv[2..][0]
name = "help" if name == undefined
_.each commandWraps, (cmd) ->
if cmd.type == type && cmd.name == name
handled = true
cmd.callback()
handled
missingPythonDeps = (commandName, packageName) ->
log " * missing #{commandName} !"
log " try... pip install #{packageName}"
log " or... easy_install install #{packageName}"
process.exit -1 |
Handle wrapping | exports.run = ->
showGreatings()
ok_callback = ->
unless handleCommand 'before' |
Well, let's dance baby | options = process.argv[2..].join ' ' |
convert options back to string | exec "couchapp #{options}", handleOutput ->
handleCommand 'after'
exec 'couchapp --version > /dev/null', handleOutput ->
config = getConfig()
[options, database] = processOptions()
if config['env'][database]['make_dumps']
exec 'couchdb-dump --version > /dev/null', handleOutput ok_callback, ->
missingPythonDeps("couchapp", "couchapp")
else
ok_callback()
, ->
missingPythonDeps "couchdb-dump", "couchdb"
|
execute couchapp command | undefined |