<commit_before>#! /usr/bin/env node
// -*- coding: utf-8 -*-
// vim: et sw=4 ts=4

var YUI = require("yui3").YUI;

YUI.add('docparser', function(Y) {

    var Lang = Y.Lang,
    trim = Lang.trim,
    stringify = Y.JSON.stringify,

    CURRENT_NAMESPACE = 'currentnamespace',
    CURRENT_MODULE = 'currentmodule',
    CURRENT_SUBMODULE = 'currentsubmodule',
    CURRENT_FILE = 'currentfile',
    CURRENT_CLASS = 'currentclass',

    REGEX_TYPE = /(.*)\{(.*)\}(.*)/,
    REGEX_FIRSTWORD = /^\s*?([^\s]+)(.*)/,
    REGEX_OPTIONAL = /\[(.*)\]/,
    REGEX_START_COMMENT = /^\s*\/\*\*/,
    REGEX_END_COMMENT = /\*\/\s*$/,
    REGEX_LINES = /\r\n|\n/,

    /**
     * A list of known tags.  This populates a member variable
     * during initialization, and will be updated if additional
     * digesters are added.
     * @property TAGLIST
     * @inner
     * @final
     */
    TAGLIST = [
        "async",  // bool, custom events can fire the listeners in a setTimeout
        "attribute", // YUI attributes -- get/set with change notification, etc
        "beta",  // module maturity identifier
        "broadcast",  // bool, events
        "bubbles", // custom events that bubble
        "category", // modules can be in multiple categories
        "chainable", // methods that return the host object
        "class", // pseudo class
        "conditional", // conditional module
        "config", // a config param (not an attribute, so no change events)
        "const", // not standardized yet, converts to final property
        "constructs", // factory methods (not yet used)
        "constructor", // this is a constructor
        "default", // property/attribute default value
        "deprecated", // please specify what to use instead
        "description", // can also be free text at the beginning of a comment is
        "emitfacade",  // bool, YUI custom event can have a dom-like event facade
        "extension", // this is an extension for [entity]
        "fireonce",  // bool, YUI custom event config allows
        "event", // YUI custom event
        "example", // 0..n code snippets.  snippets can also be embedded in the desc
        "experimental",  // module maturity identifier
        "extends", // pseudo inheritance
        "file", // file name (used by the parser)
        "final", // not meant to be changed
        "for", // used to change class context
        "global", // declare your globals
        "in", // indicates module this lives in (obsolete now)
        "initonly", // attribute writeonce value
        "knownissue", // 0..n known issues for your consumption
        "line", // line number for the comment block (used by the parser)
        "method", // a method
        "module", // YUI module name
        "namespace", // Y.namespace, used to fully qualify class names
        "optional", // for module requirements (like 'requires')
        "param", // member param
        "plugin", // this is a plugin for [entityl]
        "preventable", // YUI custom events can be preventable ala DOM events
        "private", // > access
        "property", // a regular-ole property
        "protected", // > access
        "public", // > access
        "queuable",  // bool, events
        "readonly", // YUI attribute config
        "requires", // YUI module requirements
        "return", // {type} return desc -- returns is converted to this
        "see", // 0..n things to look at
        "since", // when it was introduced
        "static",  // static
        "submodule", // YUI submodule
        "throws", // {execption type} description
        "title", // this should be something for the project description
        "todo", // 0..n things to revisit eventually (hopefully)
        "type", // the var type
        "uses", // 0..n compents mixed (usually, via augment) into the prototype
        "value", // the value of a constant
        "writeonce" // YUI attribute config
    ],

    /**
     * Common errors will get scrubbed instead of being ignored.
     * @property CORRECTIONS
     * @inner
     * @final
     */
    CORRECTIONS = {
        'augments': 'uses', // YUI convention for prototype mixins
        'depreciated': 'deprecated', // subtle difference
        'desciption': 'description', // shouldn't need the @description tag at all
        'extend': 'extends', // typo
        'function': 'method', // we may want standalone inner functions at some point
        'member': 'method', // probably meant method
        'parm': 'param', // typo
        'propery': 'property', // typo
        'returns': 'return' // need to standardize on one or the other
    },

    /**
     * A map of the default tag processors, keyed by the
     * tag name.  Multiple tags can use the same digester
     * by supplying the string name that points to the
     * implementation rather than a function.
     * @property DIGESTERS
     * @inner
     * @final
     */
    DIGESTERS = {

        // @param {type} name description    -or-
        // @param name {type} description
        // #2173362 optional w/ or w/o default
        // @param {type} [optvar=default] description
        // #12 document config objects
        // @param {object|config} config description
        // @param {type} config.prop1 description
        // @param {type} config.prop2 description
        // #11 document callback argument signature
        // @param {callback|function} callback description
        // @param {type} callback.arg1 description
        // @param {type} callback.arg2 description
        // #2173362 document event facade decorations for custom events
        // @param {event} event description
        // @param {type}  event.child description
        // @param {type}  event.index description
        'param': function(tagname, value, target, block) {
            // Y.log('param digester' + value);
            target.params = target.params || [];

            if (!value) {
Y.log('param name/type/descript missing: ' + stringify(block, null, 4), 'warn');
                return;
            }

            var type, name, desc = trim(value),
                match = REGEX_TYPE.exec(desc),
                optional, optdefault, parent,
                host = target.params;

            // Extract {type}
            if (match) {
                type = trim(match[2]);
                desc = trim(match[1] + match[3]);
            }

            // extract the first word, this is the param name
            match = REGEX_FIRSTWORD.exec(desc);
            if (match) {
                name = trim(match[1]);
                desc = trim(match[2]);
            }

            if (!name) {
Y.log('param name missing: ' + value + ':' + stringify(block, null, 4), 'warn');
                name = 'UNKNOWN';
            }

            // extract [name], optional param
            if (name.indexOf('[') > -1) {
                match = REGEX_OPTIONAL.exec(name);
                if (match) {
                    optional = true;
                    name = trim(match[1]);
                    // extract optional=defaultvalue
                    parts = name.split('=');
                    if (parts.length > 1) {
                        name = parts[0];
                        optdefault = parts[1];
                    }
                }
            }

            // parse object.prop, indicating a child property for object
            if (name.indexOf('.') > -1) {
                match = name.split('.');
                parent = trim(match[0]);

                Y.some(target.params, function(param) {
                    if (param.name == parent) {
                        param.props = param.props || [];
                        host = param.props;
                        name = trim(match[1]);
                        return true;
                    }
                });
            }

            result = {
                name: name,
                description: desc
            };

            if (type) {
                result.type = type;
            }

            if (optional) {
                result.optional = true;
                if (optdefault) {
                    result.optdefault = optdefault;
                }
            }

            host.push(result);

        },

        // @return {type} description // methods
        // @returns {type} description // methods
        // @throws {type} an error #2173342
        // can be used by anthing that has an optional {type} and a description
        'return': function(tagname, value, target, block) {

            var desc = value, type, match = REGEX_TYPE.exec(desc),
                result = {};
            if (match) {
                type = trim(match[2]);
                desc = trim(match[1] + match[3]);
            }

            result = {
                description: desc
            };

            if (type) {
                result.type = type;
            }

            target[tagname] = result;

        },
        'throws': 'return',

        // trying to overwrite the constructor value is a bad idea
        'constructor': function(tagname, value, target, block) {
            target.is_constructor = 1;
        },

        // @author {twitter: @arthurdent | github: ArthurDent}
        //    Arthur Dent adent@h2g2.earth #23, multiple // modules/class/method
        // 'author': function(tagname, value, target, block) {
        //     // Y.log('author digester');
        // },

        // A key bock type for declaring modules and submodules
        // subsequent class and member blocks will be assigned
        // to this module.
        'module': function(tagname, value, target, block) {
            this.set(CURRENT_MODULE, value);
            var go = true;
            Y.some(block, function(o) {
                if (trim(o.tag) == 'submodule') {
                    go = false;
                    return true;
                }
            });
            if (go) {
                host = this.modules[value];
                return host;
            }
            return null;
        },

        // A key bock type for declaring submodules.  subsequent class and
        // member blocks will be assigned to this submodule.
        'submodule': function(tagname, value, target, block) {
            this.set(CURRENT_SUBMODULE, value);
            var host = this.modules[value],
                parent = this.get(CURRENT_MODULE);
                if (parent) {
                    host.module = parent;
                }
            return host;
        },

        // A key bock type for declaring classes, subsequent
        // member blocks will be assigned to this class
        'class': function(tagname, value, target, block) {
            this.set(CURRENT_CLASS, value);
            var fullname = this.get(CURRENT_CLASS);
            var host = this.classes[fullname],
                parent = this.get(CURRENT_MODULE);
            if (parent) {
                host.module = parent;
            }
            parent = this.get(CURRENT_SUBMODULE);
            if (parent) {
                host.submodule = parent;
            }
            return host;
        },

        // change 'const' to final property
        'const': function(tagname, value, target, block) {
            target.itemtype = property;
            target.name = value;
            target.final = ''
        },

        // supported classitems
        'property': function(tagname, value, target, block) {
            target.itemtype = tagname;
            target.name = value;
        },
        'method': 'property',
        'attribute': 'property',
        'config': 'property',
        'event': 'property',

        // access fields
        'public': function(tagname, value, target, block) {
            target.access = tagname;
            target.tagname = value;
        },
        'private': 'public',
        'protected': 'public',
        'inner': 'public',

        // tags that can have multiple occurances in a single block
        'todo': function(tagname, value, target, block) {
            if (!Lang.isArray(target[tagname])) {
                target[tagname] = [];
            }
            target[tagname].push(value);
        },
        'example': 'todo',
        'author': 'todo',
        'see': 'todo',
        'throws': 'todo',
        'requires': 'todo',
        'knownissue': 'todo',
        'optional': 'todo',
        'uses': 'todo',
        'category': 'todo',

        // updates the current namespace
        'namespace': function(tagname, value, target, block) {
            this.set(CURRENT_NAMESPACE, value);
            var file = this.get(CURRENT_FILE);
            if (file) {
                this.files[file].namespaces[value] = 1;
            }
            var mod = this.get(CURRENT_MODULE);
            if (mod) {
                this.modules[mod].namespaces[value] = 1;
            }

            var mod = this.get(CURRENT_SUBMODULE);
            if (mod) {
                this.modules[mod].namespaces[value] = 1;
            }
        },

        // updates the current class only (doesn't create
        // a new class definition)
        'for': function(tagname, value, target, block) {
            this.set(CURRENT_CLASS, value);
            var file = this.get(CURRENT_FILE);
            if (file) {
                this.files[file].fors[value] = 1;
            }
            var mod = this.get(CURRENT_MODULE);
            if (mod) {
                this.modules[mod].fors[value] = 1;
            }

            var mod = this.get(CURRENT_SUBMODULE);
            if (mod) {
                this.modules[mod].fors[value];
            }
        }

    },

    /**
     * The doc parser accepts a map of files to file content.
     * Once parse() is called, various properties will be populated
     * with the parsers data (aggregated in the 'data' property).
     * @class DocParser
     * @constructor
     * @param o the config object
     */
    DocParser = function(o) {
        this.digesters = Y.merge(DIGESTERS);
        this.knowntags = Y.Array.hash(TAGLIST);
        DocParser.superclass.constructor.apply(this, arguments);
    };

    DocParser.NAME = 'DocParser';

    DocParser.ATTRS = {
        /**
         * Digesters process the tag/text pairs found in a
         * comment block.  They are looked up by tag name.
         * The digester gets the tagname, the value, the
         * target object to apply values to, and the full
         * block that is being processed.  Digesters can
         * be declared as strings instead of a function --
         * in that case, the program will try to look up
         * the key listed and use the function there instead
         * (it is an alias).  Digesters can return a host
         * object in the case the tag defines a new key
         * block type (modules/classes/methods/events/properties)
         * @attribute digesters
         */
        digesters: {
            setter: function(val) {
                Y.mix(this.digesters, val, true);
                Y.mix(this.knowntags, val, true);
                return val;
            }
        },

        /**
         * Emitters will be schemas for the types of payloads
         * the parser will emit.  Not implemented.
         * @attribute emitters
         */
        emitters: {
            setter: function(val) {
                Y.mix(this.emitters, val, true);
            }
        },

        /**
         * The map of file names to file content.
         * @attribute filemap
         */
        filemap: {
            writeOnce : true
        },

        /**
         * A map of file names to directory name.  Provided in
         * case this needs to be used to reset the module name
         * appropriately -- currently not used
         * @attribute dirmap
         */
        dirmap: {
            writeOnce : true
        },

        /**
         * The file currently being parsed
         * @attribute currentfile
         */
        currentfile: {
            setter: function(val) {
                val = trim(val);
                // this.set(CURRENT_NAMESPACE, '');
                if (!(val in this.files)) {
                    this.files[val] = {
                        name: val,
                        modules: {},
                        classes: {},
                        fors: {},
                        namespaces: {}
                    };
                }
                return val;
            }
        },

        /**
         * The module currently being parsed
         * @attribute currentmodule
         */
        currentmodule: {
            setter: function(val) {
                if (!val) {
                    return val;
                }
                val = trim(val);
                this.set(CURRENT_SUBMODULE, '');
                this.set(CURRENT_NAMESPACE, '');
                if (!(val in this.modules)) {
                    this.modules[val] = {
                        name: val,
                        submodules: {},
                        classes: {},
                        fors: {},
                        namespaces: {}
                    };
                }
                return val;
            }
        },

        /**
         * The submodule currently being parsed
         * @attribute currentsubmodule
         */
        currentsubmodule: {
            setter: function(val) {
                if (!val) {
                    return val;
                }
                val = trim(val);
                if (!(val in this.modules)) {
                    var mod = this.modules[val] = {
                        name: val,
                        submodules: {},
                        classes: {},
                        fors: {},
                        is_submodule: 1,
                        namespaces: {}
                    };
                    mod.module = this.get(CURRENT_MODULE);
                    mod.namespace = this.get(CURRENT_NAMESPACE);
                }
                return val;
            }
        },

        /**
         * The class currently being parsed
         * @attribute currentclass
         */
        currentclass: {
            setter: function(val) {
                if (!val) {
                    return val;
                }
                val = trim(val);
                if (!(val in this.classes)) {
                    var ns = this.get(CURRENT_NAMESPACE),
                        name = (ns) ? ns + '.' + val : val,
                        clazz = this.classes[name] = {
                            name: name,
                            shortname: val,
                            classitems: [],
                            plugins: [],
                            extensions: [],
                            plugin_for: [],
                            extension_for: []
                        };
                    clazz.module = this.get(CURRENT_MODULE);
                    clazz.submodule = this.get(CURRENT_SUBMODULE)
                    clazz.namespace = ns;
                }
                return name;
            }
        }
    };

    Y.extend(DocParser, Y.Base, {

        initializer: function() {

            var self = this;

            self.after('currentmoduleChange', function(e) {
                var mod = e.newVal, classes = self.classes;
                Y.each(classes, function(clazz) {
                    if (!(clazz.module)) {
                        clazz.module = mod;
                    }
                });
            });

            self.after('currentsubmoduleChange', function(e) {
                var mod = e.newVal, classes = self.classes,
                    parent;

                if (mod) {
                    parent = self.modules[mod].module;
                    Y.each(classes, function(clazz) {
                        if (!(clazz.submodule)) {
                            if ((!clazz.module) || clazz.module == parent) {
                                clazz.submodule = mod;
                            }
                        }
                    });
                }
            });

            self.after('currentclassChange', function(e) {
                var clazz = e.newVal;
                Y.each(self.classitems, function(item) {
                    if (!(item["class"])) {
                        item["class"] = clazz;
                    }
                });
                // Y.log(self.classitems);
            });

        },

        /**
         * Transforms a JavaDoc style comment block (less the start
         * and end of it) into a list
         * of tag/text pairs.  The leading space and '*' are removed,
         * but the remaining whitespace is preserved so that the
         * output should be friendly for both markdown and html
         * parsers.
         */
        handlecomment: function(comment, file, line) {
            var lines = comment.split(REGEX_LINES),
                len = lines.length, i,
                parts, part, peek, skip,
                results = [{tag: 'file', value: file},
                           {tag: 'line', value: line}],
                linefix = /^\s*\*\s?/;

// trim leading space and *, preserve the rest of the white space
            for (i = 0; i < len; i++) {
                lines[i] = lines[i].replace(linefix, ' ');
            }

// reconsitute and tokenize the comment block
            comment = lines.join('\n');
            parts = comment.split(/ (@\w*)/);
            len = parts.length;
            for (i = 0; i < len; i++) {
                value = '';
                part = parts[i];
                skip = false;

// the first token may be the description, otherwise it should be a tag
                if (i === 0 && part.substr(0, 1) !== '@') {
                    if (part) {
                        tag = '@description';
                        value = part;
                    } else {
                        skip = true;
                    }
                } else {
                    tag = part;
                    // lookahead for the tag value
                    peek = parts[i + 1];
                    if (peek) {
                        value = peek;
                        i++;
                    }
                }

                if (!skip && tag) {
                    results.push({
                        tag: tag.substr(1).toLowerCase(),
                        value: value
                    });
                }
            }

            return results;
        },

        /**
         * Accepts a map of filenames to file content.  Returns
         * a map of filenames to an array of API comment block
         * text.  This expects the comment to start with / **
         * on its own line, and end with &/ on its own
         * line.  Override this function to provide an
         * alternative comment parser.
         * @method extract
         * @param {Object} filemap a map of filenames to file content'
         * @return {Object} a map of filenames to an array of extracted
         * comment text.
         */
        extract: function(filemap, dirmap) {
            filemap = filemap || this.get('filemap');
            dirmap = dirmap || this.get('dirmap');
            var commentmap = {};
            Y.each(filemap, function(code, filename) {

                var commentlines, comment,
                    lines = code.split(REGEX_LINES),
                    len = lines.length, i, linenum;

                for (i = 0; i < len; i++) {
                    line = lines[i];
                    if(REGEX_START_COMMENT.test(line)) {
                        commentlines = [];

                        linenum = i + 1;

                        while (i < len && (!REGEX_END_COMMENT.test(line))) {
                            commentlines.push(line);
                            i++;
                            line = lines[i];
                        }

                        // we can look ahead here if we need to guess the
                        // name/type like we do in the python version.

                        // remove /**
                        commentlines.shift();
                        comment = commentlines.join('\n');
                        commentmap[filename] = commentmap[filename] || [];
                        commentmap[filename]
                          .push(this.handlecomment(comment, filename, linenum));
                    }
                }
            }, this);

            this.commentmap = commentmap;
            return commentmap;
        },

        /**
         * Processes all the tags in a single comment block
         * @method processblock
         * @param {Array} an array of the tag/text pairs
         */
        processblock: function(block) {
            var target = {},
                clazz,
                module,
                submodule,
                digestname,
                digester,
                host;
            // Y.log(block);
            Y.each(block, function(tag) {
                var name = trim(tag.tag),
                    value = trim(tag.value),
                    parent, ret;

                if (tag && tag.tag) {

                    if (!(name in this.knowntags)) {
                        if (name in CORRECTIONS) {
Y.log('replacing incorrect tag: ' + name + ' with ' + CORRECTIONS[name], 'warn');
// Y.log(stringify(block, null, 4), 'warn');

                            name = CORRECTIONS[name];
                        } else {
Y.log('unknown tag: ' + name + ',' + stringify(block, null, 4), 'warn');
                        }
                    }
                    digestname = name;
                    if (digestname in this.digesters) {
                        digester = this.digesters[digestname];
                        if (Lang.isString(digester)) {
                            digester = this.digesters[digester];
                        }
                        ret = digester.call(this, name, value, target, block);
                        host = host || ret;
                    } else {
                        target[name] = value;
                    }
                }

            }, this);

            if (host) {
                Y.mix(host, target);
            } else {
                this.classitems.push(target);
                target['class'] = this.get(CURRENT_CLASS);
                target.module = this.get(CURRENT_MODULE);
                host = this.get(CURRENT_SUBMODULE);
                if (host) {
                    target.submodule = host;
                }
                host = this.get(CURRENT_NAMESPACE);
                if (host) {
                    target.namespace = host;
                }
            }
        },

        /**
         * Transforms a map of filenames to arrays of comment blocks into a
         * JSON structure that represents the entire processed API doc info
         * and relationships between elements for the entire project.
         * @method transform
         * @param {object} commentmap
         * @return {object} the transformed data for the project
         */
        transform: function(commentmap) {
            var self = this,
                files = self.files = {},
                modules = self.modules = {},
                classes = self.classes = {},
                classitems = self.classitems = [],
                data = self.data = {
                    files: files,
                    modules: modules,
                    classes: classes,
                    classitems: classitems
                };

            commentmap = commentmap || self.commentmap;

            // process
            Y.each(commentmap, function(blocks, file) {
                Y.log('transform: ' + file);
                self.set(CURRENT_FILE, file);
                Y.each(blocks, function(block) {
                    self.processblock(block);
                });
            });

            // cross reference
            Y.each(modules, function(module, name) {
                if (module.file) {
                    files[module.file].modules[name] = 1;
                }
            });

            Y.each(classes, function(clazz, name) {
                if (clazz.module) {
                    modules[clazz.module].classes[name] = 1;
                }
                if (clazz.submodule) {
                    modules[clazz.submodule].classes[name] = 1;
                }
                if (clazz.file) {
                    files[clazz.file].classes[name] = 1;
                }
            });

            return self;
        },

        /**
         * Extracts and transforms the filemap provided to constructor
         * @method parse
         * @param filemap a map of filenames to file content'
         * @return {DocParser} this parser instance.  The total results
         * are available in parser.data.
         */
        parse: function(filemap, dirmap) {
            filemap = filemap || this.get('filemap');
            dirmap = dirmap || this.get('dirmap');
            return this.transform(this.extract(filemap, dirmap));
        }

    });

    Y.DocParser = DocParser;

}, '0.1.0', { requires: ['base-base', 'json-stringify'] });

/////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////

YUI({
    // filter: 'debug'
}).use('docparser', function(Y) {

var fs = require("fs"),
    sys = require("sys"),
    path = require("path"),

    options = {
        outdir: 'out',
        extension: '.js',
        exclude: '.DS_Store,.svn,CVS,.git,build_rollup_tmp,build_tmp',
        norecurse: false,
        version: '0.1.0'
    },
    args = Y.Array(process.argv, 2),
    paths = [],
    config = function() {
        while (args.length > 0) {
            var v = args.shift();
            switch (v) {
                case "-e":
                case "--extension":
                    options.extension = args.shift();
                    break;
                case "-x":
                case "--exclude":
                    options.extension = args.shift();
                    break;
                case "-v":
                case "--version":
                    options.version = args.shift();
                    break;
                case "-n":
                case "--norecurse":
                    options.norecurse = true;
                    break;
                case "-o":
                case "--outdir":
                    options.outdir = args.shift();
                    break;
                default:
                    paths.push(v);
            }
        }
        options.extensions = Y.Array.hash(options.extension.split(','));
        options.excludes = Y.Array.hash(options.exclude.split(','));
    },

    output = function(parser) {
        // Y.log(Y.JSON.stringify(parser.data, null, 4));
        var file = path.join(options.outdir, 'data.json'), out;
        try {
            fs.mkdirSync(options.outdir, 0777);
        } catch(e) { }
        out = fs.createWriteStream(file, {
                flags: "w", encoding: "utf8", mode: 0644
        });
        out.write(Y.JSON.stringify(parser.data, null, 4));
        out.end();
    },

    filemap = {},
    dirmap = {},

    run = function() {
        output(new Y.DocParser({
            filemap: filemap,
            dirmap: dirmap
        }).parse());
    },

    parsefiles = function(dir, files) {
        Y.each(files, function(filename) {
            var dot = filename.indexOf('.'), ext, text, fullpath;
            if (dot) {
                ext = filename.substr(dot);
                if (ext in options.extensions) {
                    fullpath = path.join(dir, filename);
                    text = fs.readFileSync(fullpath, "utf8");
                    filemap[fullpath] = text;
                    dirmap[fullpath] = dir;
                }
            }
        });
    },

    parsedir = function(dir) {
        var allfiles = fs.readdirSync(dir), stats,
            files = [], fullpath;
        Y.each(allfiles, function(filename) {
            if (!(filename in options.excludes)) {
                fullpath = path.join(dir, filename);
                stats = fs.statSync(fullpath);
                if (stats.isDirectory() && !options.norecurse) {
                    parsedir(fullpath);
                } else {
                    files.push(filename);
                }
            }
        });

        parsefiles(dir, files);
    },

    load = function() {
        Y.each(paths, function(dir) {
            parsedir(dir);
        });
    };

    config();
    load();
    run();

});

<commit_msg>Added some path.exists checks, path.joins and path.extname<commit_after>#! /usr/bin/env node
// -*- coding: utf-8 -*-
// vim: et sw=4 ts=4

var YUI = require("yui3").YUI,
    fs = require("fs"),
    sys = require("sys"),
    path = require("path");

YUI({
    modules: {
        docparser: {
            fullpath: path.join(__dirname,  'docparser.js'),
            requires: ['base-base', 'json-stringify']
        }
    }
    // filter: 'debug'
}).use('docparser', function(Y) {

    var options = {
        outdir: path.join(process.cwd(),  'out'),
        extension: '.js',
        exclude: '.DS_Store,.svn,CVS,.git,build_rollup_tmp,build_tmp',
        norecurse: false,
        version: '0.1.0'
    },
    args = Y.Array(process.argv, 2),
    paths = [],
    config = function() {
        while (args.length > 0) {
            var v = args.shift();
            switch (v) {
                case "-e":
                case "--extension":
                    options.extension = args.shift();
                    break;
                case "-x":
                case "--exclude":
                    options.extension = args.shift();
                    break;
                case "-v":
                case "--version":
                    options.version = args.shift();
                    break;
                case "-n":
                case "--norecurse":
                    options.norecurse = true;
                    break;
                case "-o":
                case "--outdir":
                    options.outdir = args.shift();
                    break;
                default:
                    paths.push(v);
            }
        }
        options.extensions = Y.Array.hash(options.extension.split(','));
        options.excludes = Y.Array.hash(options.exclude.split(','));
    },

    output = function(parser) {
        // Y.log(Y.JSON.stringify(parser.data, null, 4));
        var file = path.join(options.outdir, 'data.json'), out;
        if (!path.existsSync(options.outdir)) {
            Y.log('Making out dir: ' + options.outdir, 'warn', 'yuidoc');
            fs.mkdirSync(options.outdir, 0777);
        }
        out = fs.createWriteStream(file, {
                flags: "w", encoding: "utf8", mode: 0644
        });
        out.write(Y.JSON.stringify(parser.data, null, 4));
        out.end();
    },

    filemap = {},
    dirmap = {},

    run = function() {
        output(new Y.DocParser({
            filemap: filemap,
            dirmap: dirmap
        }).parse());
    },

    parsefiles = function(dir, files) {
        Y.each(files, function(filename) {
            var ext = path.extname(filename), text, fullpath;
            if (ext) {
                if (ext in options.extensions) {
                    fullpath = path.join(dir, filename);
                    if (path.existsSync(fullpath)) {
                        text = fs.readFileSync(fullpath, "utf8");
                        filemap[fullpath] = text;
                        dirmap[fullpath] = dir;
                    } else {
                        Y.log('File skipped: ' + fullpath, 'warn', 'yuidoc');
                    }
                }
            }
        });
    },

    parsedir = function(dir) {
        //Do a little dir cleanup..
        if (dir.indexOf(process.cwd()) === -1) {
            dir = path.join(process.cwd(), dir);
        }

        var allfiles = fs.readdirSync(dir), stats,
            files = [], fullpath;
        Y.each(allfiles, function(filename) {
            if (!(filename in options.excludes)) {
                fullpath = path.join(dir, filename);
                stats = fs.statSync(fullpath);
                if (stats.isDirectory() && !options.norecurse) {
                    parsedir(fullpath);
                } else {
                    files.push(filename);
                }
            }
        });

        parsefiles(dir, files);
    },

    load = function() {
        Y.each(paths, function(dir) {
            parsedir(dir);
        });
    };

    config();
    load();
    run();

});

<|endoftext|>


<commit_before>/**
 * XadillaX created at 2014-09-09 14:21
 *
 * Copyright (c) 2015 Souche.com, all rights
 * reserved.
 */
"use strict";

var util = require("util");

var async = require("async");
var mysql = require("mysql2");
var 囍 = require("lodash");

var fieldType = require("./field_type");
var ResultPromisor = require("./result_promisor");

/**
 * Yakumo Yukari is the entity of Yakumo Toshihiko.
 * @param model
 * @param source
 * @constructor
 */
var Yukari = function(model, source) {
    this.$model = model;
    this.$toshihiko = model.toshihiko;

    this.$fields = model._fieldsObject;
    this.$origData = {};

    this.$source = source;

    this.$dbName = this.$toshihiko.database;
    this.$tableName = this.$model.name;
    this.$cache = model.cache;

    this.$fromCache = false;
};

/**
 * get primary keys and values
 * @param {Boolean} [fromOrig] is primary keys from original data
 * @returns {*}
 * @private
 */
Yukari.prototype._getPrimaryKeys = function(fromOrig) {
    var keys = this.$model.getPrimaryKeysName();
    if(typeof keys === "string") {
        return (fromOrig || this[keys] === undefined) ?
            this.$origData[keys] :
            this[keys];
    }

    var obj = {};
    for(var i = 0; i < keys.length; i++) {
        obj[keys[i]] = (fromOrig || this[keys[i]] === undefined) ?
            this.$origData[keys[i]] :
            this[keys[i]];
    }
    return obj;
};

/**
 * build row
 * @param row
 * @param useOrigName
 * @private
 */
Yukari.prototype._buildRow = function(row, useOrigName) {
    this.$origData = {};

    for(var i = 0; i < this.$fields.length; i++) {
        var field = this.$fields[i];
        var value = row[useOrigName ? field.column : field.name];

        if(undefined === value) {
            if(undefined !== field.defaultValue) {
                value = field.defaultValue;
            }
        }

        if(undefined === value) {
            continue;
        }

        this[field.name] = 囍.clone(value);
    }

    if(row.$fromCache) {
        this.$fromCache = true;
    }
};

/**
 * initialize row
 * @param row
 * @param useOrigName
 * @private
 */
Yukari.prototype._initRow = function(row, useOrigName) {
    for(var i = 0; i < this.$fields.length; i++) {
        var field = this.$fields[i];
        var name = useOrigName ? field.column : field.name;
        if(undefined === row[name]) {
            continue;
        }

        this.$origData[field.name] = {
            fieldIdx : i,
            data     : row[name] === null ? null : field.type.parse(row[name])
        };
    }

    for(var key in this.$origData) {
        if(!this.$origData.hasOwnProperty(key)) {
            continue;
        }

        this[key] = 囍.clone(this.$origData[key].data, true);
    }

    if(row.$fromCache) {
        this.$fromCache = true;
    }
};

/**
 * to json object
 * @param [old]
 * @returns {{}}
 */
Yukari.prototype.toJSON = function(old) {
    var obj = old ? this.$origData : this;
    var result = {};
    for(var key in obj) {
        if(key.length && key[0] !== "$" && typeof obj[key] !== "function") {
            result[key] = obj[key];

            // if it has a certain `toJSON` function
            var idx = this._fieldAt(key);
            if(idx !== -1 && typeof this.$fields[idx].type.toJSON === "function") {
                result[key] = this.$fields[idx].type.toJSON(result[key]);
            }
        }
    }

    return result;
};

/**
 * field at
 * @param name
 * @returns {*}
 * @private
 */
Yukari.prototype._fieldAt = function(name) {
    if(this.$source !== "new" && this.$origData[name]) {
        return this.$origData[name].fieldIdx;
    } else if(this.$source === "new") {
        for(var i = 0; i < this.$fields.length; i++) {
            if(this.$fields[i].name === name) {
                return i;
            }
        }
    }

    return -1;
};

/**
 * validate all fields
 * @returns {*}
 */
Yukari.prototype.validateAll = function() {
    for(var key in this) {
        if(!this.hasOwnProperty(key)) {
            continue;
        }

        if(key[0] === "$" ||
            typeof this[key] === "function" ||
            this._fieldAt(key) === -1) {
            continue;
        }

        var result = this.validateOne(key, this[key]);
        if(typeof result === "string" && result.length) {
            return result;
        }
    }

    return "";
};

/**
 * validate one field
 * @param name
 * @param value
 * @returns {*}
 */
Yukari.prototype.validateOne = function(name, value) {
    var fieldIdx = this._fieldAt(name);
    if(-1 === fieldIdx) {
        return "No such field.";
    }

    var field = this.$fields[fieldIdx];

    if(null === value && !field.allowNull) {
        return "Field " + name + " can't be NULL.";
    } else if(null === value && field.allowNull) {
        return "";
    }

    for(var i = 0; i < field.validators.length; i++) {
        var result = field.validators[i](value);
        if(typeof result === "string" && result.length) {
            return result;
        }
    }

    return "";
};

/**
 * insert a record
 * @param callback
 * @returns {*}
 */
Yukari.prototype.insert = function(callback) {
    var promise = new ResultPromisor();
    if(callback === undefined) {
        callback = function() {};
    }

    if(this.$source !== "new") {
        callback(new Error(
            "You must call this function via a new Yukari object."));

        promise._onExecuteResult(new Error(
            "You must call this function via a new Yukari object."));

        return promise;
    }

    var vldt = this.validateAll();
    if(vldt.length) {
        callback(new Error(vldt));
        promise._onExecuteResult(new Error(vldt));
        return promise;
    }

    var data = {};
    for(var key in this) {
        if(!this.hasOwnProperty(key)) {
            continue;
        }

        if(key[0] === "$") {
            continue;
        }

        var fieldIdx = this._fieldAt(key);

        if(-1 === fieldIdx) {
            continue;
        }

        var field = this.$fields[fieldIdx];
        if(this[key] !== null) {
            data[field.column] = field.type.restore(this[key]);
        } else {
            this[key] = null;
        }
    }

    var sql = util.format("INSERT INTO `%s` SET ?", this.$model.name);
    sql = mysql.format(sql, data);

    if(this.$toshihiko.options.showSql) {
        console.log("❤ Toshihiko is executing SQL: 【" + sql + "】...");
    }

    var self = this;
    this.$toshihiko.execute(sql, function(err, row) {
        if(err) {
            callback(err, undefined, sql);
            promise._onExecuteResult(err, undefined, sql);
            return promise;
        }

        if(!row) {
            callback(new Error("No row inserted."), undefined, sql);
            promise._onExecuteResult(
                new Error("No row inserted."),
                undefined,
                sql);
            return promise;

        }

        self.$source = "query";

        var where = {};
        var pk = self.$model.primaryKeys;
        if(row.insertId) {
            where[pk[0].name] = row.insertId;
        } else {
            for(var i = 0; i < self.$model.primaryKeys.length; i++) {
                where[pk[0].name] = self[pk[0].name];
            }
        }

        self.$model.where(where).findOne(function(err, row) {
            if(err) {
                callback(new Error(
                    "Insert successfully but failed to read the record: " + err.message));
                promise._onExecuteResult(new Error(
                    "Insert successfully but failed to read the record: " + err.message));
                return promise;
            }

            if(!row) {
                callback(new Error(
                    "Insert successfully but failed to read the record: Unknown error."));
                promise._onExecuteResult(new Error(
                    "Insert successfully but failed to read the record: Unknown error."));
                return promise;
            }

            for(var key in row) {
                if(!row.hasOwnProperty(key)) {
                    continue;
                }

                if(key[0] === "$" && key !== "$origData") {
                    continue;
                }

                if(typeof row[key] === "function") {
                    continue;
                }

                self[key] = 囍.clone(row[key], true);
            }

            callback(undefined, self, sql);
            promise._onExecuteResult(undefined, self, sql);
        });
    });
    return promise;
};

/**
 * updata a record
 * @param callback
 * @returns {*}
 */
Yukari.prototype.update = function(callback) {
    var promise = new ResultPromisor();
    if(undefined === callback) {
        callback = function() {};
    }

    if(this.$source !== "query") {
        callback(new Error(
            "You must call this function via a Yukari object which is queried."));
        promise._onExecuteResult(new Error(
            "You must call this function via a Yukari object which is queried."));
        return promise;
    }

    var vldt = this.validateAll();
    if(vldt.length) {
        callback(new Error(vldt));
        promise._onExecuteResult(new Error(vldt));
        return promise;
    }

    var data = {};

    for(var key in this) {
        if(!this.hasOwnProperty(key)) {
            continue;
        }

        if(key[0] === "$") {
            continue;
        }

        if(typeof this[key] === "function") {
            continue;
        }

        var fieldIdx = this._fieldAt(key);
        if(-1 === fieldIdx) {
            continue;
        }

        var field = this.$fields[fieldIdx];
        var equalFunc = ((undefined === field.type.equal) ?
            fieldType.$equal.bind(fieldType) :
            field.type.equal.bind(field.type));

        // only change changed data
        if(!equalFunc(this[key], this.$origData[key].data)) {
            data[field.name] = {
                data    : this[key],
                field   : field
            };
        }
    }

    // FIX: if no data changed, then change all data
    if(!囍.size(data)) {
        for(var key in this) {
            if(!this.hasOwnProperty(key)) continue;
            if(key[0] === "$") continue;
            if(typeof this[key] === "function") continue;

            var fieldIdx = this._fieldAt(key);
            if(-1 === fieldIdx) continue;

            var field = this.$fields[fieldIdx];
            data[field.name] = {
                data: this[key],
                field: field
            };
        }
    }

    // set data
    var sql = "UPDATE `%s` SET ?";
    var finalData = {};
    for(var key in data) {
        if(!data.hasOwnProperty(key)) continue;

        var n2c = this.$model._fieldsKeyMap.n2c[key];
        if(data[key].data !== null) {
            finalData[n2c] = data[key].field.type.restore(data[key].data);
        } else {
            finalData[n2c] = null;
        }
    }

    sql = util.format(sql, this.$model.name);
    sql = mysql.format(sql, finalData) + " WHERE ";

    // set primary key condition
    var conditionSql = "";
    var pk = [];
    for(var i = 0; i < this.$model.primaryKeys.length; i++) {
        var p = this.$model.primaryKeys[i];
        if(pk.length) {
            conditionSql += " AND ";
        }

        conditionSql += "`" + p.column + "` = ?";

        pk.push(p.type.restore(this.$origData[p.name].data));
    }
    sql += mysql.format(conditionSql, pk);

    if(this.$toshihiko.options.showSql) {
        console.log("❤ Toshihiko is executing SQL: 【" + sql + "】...");
    }

    var self = this;
    async.waterfall([
        /**
         * step 1.
         *   if cache...
         * @param callback
         */
        function(callback) {
            if(!self.$cache) {
                return callback();
            }

            var primary = self._getPrimaryKeys(true);
            if(typeof primary === "object") {
                self.$model.nameToColumn(primary);
            }

            self.$cache.deleteData(
                self.$dbName,
                self.$tableName,
                primary,
                function(err) {
                    callback(err);
                });
        },

        /**
         * step 2.
         *   edit 本体...
         * @param callback
         */
        function(callback) {
            self.$toshihiko.execute(sql, function(err, results) {
                if(err) {
                    return callback(err);
                }

                if(!results.affectedRows) {
                    return callback(new Error("No row(s) effected."));
                }

                for(var key in data) {
                    if(!data.hasOwnProperty(key)) {
                        continue;
                    }

                    var d = data[key];
                    self.$origData[d.field.name].data = d.data;
                }
                self.$source = "query";

                callback();
            });
        }
    ], function(err) {
        callback(err, self, sql);
        promise._onExecuteResult(err, self, sql);
    });
    return promise;
};

/**
 * update by json
 * @param jsonData
 * @param callback
 */
Yukari.prototype.updateByJson = function(jsonData, callback) {
    if ("function" === typeof jsonData) {
        callback = jsonData;
        jsonData = {};
    }
    if (typeof jsonData !== "object") {
        return callback(new Error("Json data is not object"));
    }

    for (var key in jsonData) {
        if(!this.hasOwnProperty(key)) {
            continue;
        }
        var fieldIdx = this._fieldAt(key);
        if(-1 === fieldIdx) {
            continue;
        }
        this[key] = jsonData[key];
    }
    return this.update(callback);
};

/**
 * delete this yukari
 * @param callback
 * @returns {*}
 */
Yukari.prototype.delete = function(callback) {
    var promise = new ResultPromisor();
    if(undefined === callback) {
        callback = function(){};
    }

    if(this.$source === "new") {
        callback(new Error("You must delete a queried record."));
        promise._onExecuteResult(new Error("You must delete a queried record."));
        return promise;
    }

    var pk = {};
    for(var i = 0; i < this.$model.primaryKeys.length; i++) {
        var field = this.$model.primaryKeys[i];
        pk[field.name] = this.$origData[field.name].data;
    }

    /**
     * Model::where -> new Query
     * Query::delete 已经有做对 CACHE 的处理，无需多处理
     */
    this.$model.where(pk).limit("0,1").delete(function(err, result, sql) {
        if(err) {
            callback(err, undefined, sql);
            promise._onExecuteResult(err, undefined, sql);
        }

        callback(undefined, result.affectedRows, sql);
        promise._onExecuteResult(undefined, result.affectedRows, sql);
    });
    return promise;
};

/**
 * save / insert a record
 * @param callback
 */
Yukari.prototype.save = function(callback) {
    if(undefined === callback) {
        callback = function() {};
    }

    if(this.$source === "query") {
        return this.update(callback);
    } else if(this.$source === "new") {
        return this.insert(callback);
    }
};

module.exports = Yukari;
<commit_msg>fix: fix #38<commit_after>/**
 * XadillaX created at 2014-09-09 14:21
 *
 * Copyright (c) 2015 Souche.com, all rights
 * reserved.
 */
"use strict";

var util = require("util");

var async = require("async");
var mysql = require("mysql2");
var 囍 = require("lodash");

var fieldType = require("./field_type");
var ResultPromisor = require("./result_promisor");

/**
 * Yakumo Yukari is the entity of Yakumo Toshihiko.
 * @param model
 * @param source
 * @constructor
 */
var Yukari = function(model, source) {
    this.$model = model;
    this.$toshihiko = model.toshihiko;

    this.$fields = model._fieldsObject;
    this.$origData = {};

    this.$source = source;

    this.$dbName = this.$toshihiko.database;
    this.$tableName = this.$model.name;
    this.$cache = model.cache;

    this.$fromCache = false;
};

/**
 * get primary keys and values
 * @param {Boolean} [fromOrig] is primary keys from original data
 * @returns {*}
 * @private
 */
Yukari.prototype._getPrimaryKeys = function(fromOrig) {
    var keys = this.$model.getPrimaryKeysName();
    if(typeof keys === "string") {
        return (fromOrig || this[keys] === undefined) ?
            this.$origData[keys] :
            this[keys];
    }

    var obj = {};
    for(var i = 0; i < keys.length; i++) {
        obj[keys[i]] = (fromOrig || this[keys[i]] === undefined) ?
            this.$origData[keys[i]] :
            this[keys[i]];
    }
    return obj;
};

/**
 * build row
 * @param row
 * @param useOrigName
 * @private
 */
Yukari.prototype._buildRow = function(row, useOrigName) {
    this.$origData = {};

    for(var i = 0; i < this.$fields.length; i++) {
        var field = this.$fields[i];
        var value = row[useOrigName ? field.column : field.name];

        if(undefined === value) {
            if(undefined !== field.defaultValue) {
                value = field.defaultValue;
            }
        }

        if(undefined === value) {
            continue;
        }

        this[field.name] = 囍.clone(value);
    }

    if(row.$fromCache) {
        this.$fromCache = true;
    }
};

/**
 * initialize row
 * @param row
 * @param useOrigName
 * @private
 */
Yukari.prototype._initRow = function(row, useOrigName) {
    for(var i = 0; i < this.$fields.length; i++) {
        var field = this.$fields[i];
        var name = useOrigName ? field.column : field.name;
        if(undefined === row[name]) {
            continue;
        }

        this.$origData[field.name] = {
            fieldIdx : i,
            data     : row[name] === null ? null : field.type.parse(row[name])
        };
    }

    for(var key in this.$origData) {
        if(!this.$origData.hasOwnProperty(key)) {
            continue;
        }

        this[key] = 囍.clone(this.$origData[key].data, true);
    }

    if(row.$fromCache) {
        this.$fromCache = true;
    }
};

/**
 * to json object
 * @param [old]
 * @returns {{}}
 */
Yukari.prototype.toJSON = function(old) {
    var obj = old ? this.$origData : this;
    var result = {};
    for(var key in obj) {
        if(key.length && key[0] !== "$" && typeof obj[key] !== "function") {
            result[key] = obj[key];

            // if it has a certain `toJSON` function
            var idx = this._fieldAt(key);
            if(idx !== -1 && typeof this.$fields[idx].type.toJSON === "function") {
                result[key] = this.$fields[idx].type.toJSON(result[key]);
            }
        }
    }

    return result;
};

/**
 * field at
 * @param name
 * @returns {*}
 * @private
 */
Yukari.prototype._fieldAt = function(name) {
    if(this.$source !== "new" && this.$origData[name]) {
        return this.$origData[name].fieldIdx;
    } else if(this.$source === "new") {
        for(var i = 0; i < this.$fields.length; i++) {
            if(this.$fields[i].name === name) {
                return i;
            }
        }
    }

    return -1;
};

/**
 * validate all fields
 * @returns {*}
 */
Yukari.prototype.validateAll = function() {
    for(var key in this) {
        if(!this.hasOwnProperty(key)) {
            continue;
        }

        if(key[0] === "$" ||
            typeof this[key] === "function" ||
            this._fieldAt(key) === -1) {
            continue;
        }

        var result = this.validateOne(key, this[key]);
        if(typeof result === "string" && result.length) {
            return result;
        }
    }

    return "";
};

/**
 * validate one field
 * @param name
 * @param value
 * @returns {*}
 */
Yukari.prototype.validateOne = function(name, value) {
    var fieldIdx = this._fieldAt(name);
    if(-1 === fieldIdx) {
        return "No such field.";
    }

    var field = this.$fields[fieldIdx];

    if(null === value && !field.allowNull) {
        return "Field " + name + " can't be NULL.";
    } else if(null === value && field.allowNull) {
        return "";
    }

    for(var i = 0; i < field.validators.length; i++) {
        var result = field.validators[i](value);
        if(typeof result === "string" && result.length) {
            return result;
        }
    }

    return "";
};

/**
 * insert a record
 * @param callback
 * @returns {*}
 */
Yukari.prototype.insert = function(callback) {
    var promise = new ResultPromisor();
    if(callback === undefined) {
        callback = function() {};
    }

    if(this.$source !== "new") {
        callback(new Error(
            "You must call this function via a new Yukari object."));

        promise._onExecuteResult(new Error(
            "You must call this function via a new Yukari object."));

        return promise;
    }

    var vldt = this.validateAll();
    if(vldt.length) {
        callback(new Error(vldt));
        promise._onExecuteResult(new Error(vldt));
        return promise;
    }

    var data = {};
    for(var key in this) {
        if(!this.hasOwnProperty(key)) {
            continue;
        }

        if(key[0] === "$") {
            continue;
        }

        var fieldIdx = this._fieldAt(key);

        if(-1 === fieldIdx) {
            continue;
        }

        var field = this.$fields[fieldIdx];
        if(this[key] !== null) {
            data[field.column] = field.type.restore(this[key]);
        } else {
            this[key] = null;
        }
    }

    var sql = util.format("INSERT INTO `%s` SET ?", this.$model.name);
    sql = mysql.format(sql, data);

    if(this.$toshihiko.options.showSql) {
        console.log("❤ Toshihiko is executing SQL: 【" + sql + "】...");
    }

    var self = this;
    this.$toshihiko.execute(sql, function(err, row) {
        if(err) {
            callback(err, undefined, sql);
            promise._onExecuteResult(err, undefined, sql);
            return promise;
        }

        if(!row) {
            callback(new Error("No row inserted."), undefined, sql);
            promise._onExecuteResult(
                new Error("No row inserted."),
                undefined,
                sql);
            return promise;

        }

        self.$source = "query";

        var where = {};
        var pk = self.$model.primaryKeys;
        var ai = self.$model.ai;
        if(row.insertId) {
            // 只有一个主键
            if(pk.length === 1) {
                if(!ai || ai.primaryKey) {
                    // Model 没定义过 AI，或者定义的 AI 就是主键
                    where[pk[0].name] = row.insertId;
                } else {
                    // Model 定义过 AI 且不是主键
                    where[pk[0].name] = (self[pk[0].name] !== undefined) ?
                        self[pk[0].name] : self[pk[0].column];
                }
            } else {
                // 有多个主键

                for(var i = 0; i < self.$model.primaryKeys.length; i++) {
                    where[pk[0].name] = (self[pk[i].name] !== undefined) ?
                        self[pk[i].name] : self[pk[i].column];
                }

                // 如果有 AI 主键那么覆盖一下
                if(ai && ai.primaryKey) {
                    self[ai.name] = row.insertId;
                }
            }
        }

        self.$model.where(where).findOne(function(err, row) {
            if(err) {
                callback(new Error(
                    "Insert successfully but failed to read the record: " + err.message));
                promise._onExecuteResult(new Error(
                    "Insert successfully but failed to read the record: " + err.message));
                return promise;
            }

            if(!row) {
                callback(new Error(
                    "Insert successfully but failed to read the record: Unknown error."));
                promise._onExecuteResult(new Error(
                    "Insert successfully but failed to read the record: Unknown error."));
                return promise;
            }

            for(var key in row) {
                if(!row.hasOwnProperty(key)) {
                    continue;
                }

                if(key[0] === "$" && key !== "$origData") {
                    continue;
                }

                if(typeof row[key] === "function") {
                    continue;
                }

                self[key] = 囍.clone(row[key], true);
            }

            callback(undefined, self, sql);
            promise._onExecuteResult(undefined, self, sql);
        });
    });
    return promise;
};

/**
 * updata a record
 * @param callback
 * @returns {*}
 */
Yukari.prototype.update = function(callback) {
    var promise = new ResultPromisor();
    if(undefined === callback) {
        callback = function() {};
    }

    if(this.$source !== "query") {
        callback(new Error(
            "You must call this function via a Yukari object which is queried."));
        promise._onExecuteResult(new Error(
            "You must call this function via a Yukari object which is queried."));
        return promise;
    }

    var vldt = this.validateAll();
    if(vldt.length) {
        callback(new Error(vldt));
        promise._onExecuteResult(new Error(vldt));
        return promise;
    }

    var data = {};

    for(var key in this) {
        if(!this.hasOwnProperty(key)) {
            continue;
        }

        if(key[0] === "$") {
            continue;
        }

        if(typeof this[key] === "function") {
            continue;
        }

        var fieldIdx = this._fieldAt(key);
        if(-1 === fieldIdx) {
            continue;
        }

        var field = this.$fields[fieldIdx];
        var equalFunc = ((undefined === field.type.equal) ?
            fieldType.$equal.bind(fieldType) :
            field.type.equal.bind(field.type));

        // only change changed data
        if(!equalFunc(this[key], this.$origData[key].data)) {
            data[field.name] = {
                data    : this[key],
                field   : field
            };
        }
    }

    // FIX: if no data changed, then change all data
    if(!囍.size(data)) {
        for(var key in this) {
            if(!this.hasOwnProperty(key)) continue;
            if(key[0] === "$") continue;
            if(typeof this[key] === "function") continue;

            var fieldIdx = this._fieldAt(key);
            if(-1 === fieldIdx) continue;

            var field = this.$fields[fieldIdx];
            data[field.name] = {
                data: this[key],
                field: field
            };
        }
    }

    // set data
    var sql = "UPDATE `%s` SET ?";
    var finalData = {};
    for(var key in data) {
        if(!data.hasOwnProperty(key)) continue;

        var n2c = this.$model._fieldsKeyMap.n2c[key];
        if(data[key].data !== null) {
            finalData[n2c] = data[key].field.type.restore(data[key].data);
        } else {
            finalData[n2c] = null;
        }
    }

    sql = util.format(sql, this.$model.name);
    sql = mysql.format(sql, finalData) + " WHERE ";

    // set primary key condition
    var conditionSql = "";
    var pk = [];
    for(var i = 0; i < this.$model.primaryKeys.length; i++) {
        var p = this.$model.primaryKeys[i];
        if(pk.length) {
            conditionSql += " AND ";
        }

        conditionSql += "`" + p.column + "` = ?";

        pk.push(p.type.restore(this.$origData[p.name].data));
    }
    sql += mysql.format(conditionSql, pk);

    if(this.$toshihiko.options.showSql) {
        console.log("❤ Toshihiko is executing SQL: 【" + sql + "】...");
    }

    var self = this;
    async.waterfall([
        /**
         * step 1.
         *   if cache...
         * @param callback
         */
        function(callback) {
            if(!self.$cache) {
                return callback();
            }

            var primary = self._getPrimaryKeys(true);
            if(typeof primary === "object") {
                self.$model.nameToColumn(primary);
            }

            self.$cache.deleteData(
                self.$dbName,
                self.$tableName,
                primary,
                function(err) {
                    callback(err);
                });
        },

        /**
         * step 2.
         *   edit 本体...
         * @param callback
         */
        function(callback) {
            self.$toshihiko.execute(sql, function(err, results) {
                if(err) {
                    return callback(err);
                }

                if(!results.affectedRows) {
                    return callback(new Error("No row(s) effected."));
                }

                for(var key in data) {
                    if(!data.hasOwnProperty(key)) {
                        continue;
                    }

                    var d = data[key];
                    self.$origData[d.field.name].data = d.data;
                }
                self.$source = "query";

                callback();
            });
        }
    ], function(err) {
        callback(err, self, sql);
        promise._onExecuteResult(err, self, sql);
    });
    return promise;
};

/**
 * update by json
 * @param jsonData
 * @param callback
 */
Yukari.prototype.updateByJson = function(jsonData, callback) {
    if ("function" === typeof jsonData) {
        callback = jsonData;
        jsonData = {};
    }
    if (typeof jsonData !== "object") {
        return callback(new Error("Json data is not object"));
    }

    for (var key in jsonData) {
        if(!this.hasOwnProperty(key)) {
            continue;
        }
        var fieldIdx = this._fieldAt(key);
        if(-1 === fieldIdx) {
            continue;
        }
        this[key] = jsonData[key];
    }
    return this.update(callback);
};

/**
 * delete this yukari
 * @param callback
 * @returns {*}
 */
Yukari.prototype.delete = function(callback) {
    var promise = new ResultPromisor();
    if(undefined === callback) {
        callback = function(){};
    }

    if(this.$source === "new") {
        callback(new Error("You must delete a queried record."));
        promise._onExecuteResult(new Error("You must delete a queried record."));
        return promise;
    }

    var pk = {};
    for(var i = 0; i < this.$model.primaryKeys.length; i++) {
        var field = this.$model.primaryKeys[i];
        pk[field.name] = this.$origData[field.name].data;
    }

    /**
     * Model::where -> new Query
     * Query::delete 已经有做对 CACHE 的处理，无需多处理
     */
    this.$model.where(pk).limit("0,1").delete(function(err, result, sql) {
        if(err) {
            callback(err, undefined, sql);
            promise._onExecuteResult(err, undefined, sql);
        }

        callback(undefined, result.affectedRows, sql);
        promise._onExecuteResult(undefined, result.affectedRows, sql);
    });
    return promise;
};

/**
 * save / insert a record
 * @param callback
 */
Yukari.prototype.save = function(callback) {
    if(undefined === callback) {
        callback = function() {};
    }

    if(this.$source === "query") {
        return this.update(callback);
    } else if(this.$source === "new") {
        return this.insert(callback);
    }
};

module.exports = Yukari;
<|endoftext|>


<commit_before>#!/usr/bin/env node

var Parse = require('parse/node').Parse; // for parse
var Rx = require('rx'); // for rx
var configPath = process.env.HOME + '/.parse/' + 'config.json'; // $ docker-parse list 8tory_dev
var config = hasFile(configPath) ? require(configPath) : null;
var program = require('commander'); // for cli args
var Tag = Parse.Object.extend('Tag');
var Post = Parse.Object.extend('Post');

program
  .version('1.0.0')
  .option('-p, --production', 'production')
  .option('-a, --appId <appId>', 'appId')
  .option('-j, --jsKey <jsKey>', 'jsKey')
  .option('-m, --masterKey <masterKey>', 'masterKey')
  //.option('-d, --debug', 'Show debug messages')
  //.option('-i, --info', 'Show info only')
  //.option('-vv, --verbose', 'Show more messages')
  //.option('--years_ago <YEARS_AGO>', 'sync <YEARS_AGO> today 00:00:00 in user\'s timezone')
  //.option('--days <DAYS>', 'sync <DAYS> from <YEARS_AGO>')
  //.option('--since <SINCE>', 'A Unix timestamp or strtotime data value that points to the start of the range of time-based data')
  //.option('--until <UNTIL>', 'A Unix timestamp or strtotime data value that points to the end of the range of time-based data')
  //.option('--location', 'sync only location posts')
  //.option('-u, --user <USER>', 'User\'s object ID')
  .parse(process.argv);

var appId = program.appId ? program.appId : process.env.APP_ID;
var jsKey = program.jsKey ? program.jsKey : process.env.JS_KEY;
var masterKey = program.masterKey ? program.masterKey : process.env.MASTER_KEY;

console.log(hasFile(configPath));
if (config) {
  if (program.production) {
    console.warn('production');
    appId     = appId ? appId : config.production.appId;
    jsKey     = jsKey ? jsKey : config.production.jsKey;
    masterKey = masterKey ? masterKey : config.production.masterKey;
  } else {
    console.warn('dev');
    appId     = appId ? appId : config.dev.appId;
    jsKey     = jsKey ? jsKey : config.dev.jsKey;
    masterKey = masterKey ? masterKey : config.dev.masterKey;
  }
} else {
  console.warn('missing ' + configPath);
}

// TODO fatal error and exit
if (!appId) console.error('missing appId');
if (!jsKey) console.error('missing jsKey');
if (!masterKey) console.error('missing masterKey');

Parse.initialize(appId, jsKey, masterKey);

Parse.Cloud.useMasterKey();

// We should use message_tags from Facebook, not parse message field that's prefix with #
//
// ref.https://developers.facebook.com/docs/graph-api/reference/v2.6/post
//
// {
//   "message_tags": {
//     "{tag-key}": object,
//     "id": string,
//     "name": string,
//     "type": enum{user, page, group},
//     "offset": integer,
//     "length": integer,
//   }
// }
//

var query = new Parse.Query("Post");
//query.equalTo("isMainPost", true);
query.equalTo("source", "Fb");
query.notEqualTo("message", "");
query.ascending('createdAt'); // required for all(Parse.Query)
//var Post = Parse.Object.extend('Post');
query.include('tagList');

all(query).doOnNext(function(post) {
  console.log('before: ');
  console.log(post);
  console.log(post.get('message'));
  console.log(post.get('tagList'));
  //var tags = post.get('tagList');
  //for (var i = 0; i < tags.length; ++i) {
    //console.log(tags[i].hashtag);
  //}
}).concatMap(function (post) {
  var msg = post.get('message');
  if (!msg) return Rx.Observable.empty();
  var tokens = msg.match(/\#([^\u3000-\u303F。，\s]|[\w_-])+/g);
  return tokens ? Rx.Observable.from(tokens).concatMap(function (token) {
    var tagQuery = new Parse.Query('Tag');
    tagQuery.equalTo('hashtag', token);

    return Rx.Observable.fromPromise(tagQuery.first()).defaultIfEmpty(null).concatMap(function (tag) { // simulate switchIfEmpty(Rx.Observable)
      if (!tag) {
        var t = new Tag(); // for default
        t.set('type', 'hashtag');
        t.set('hashtag', token);
        console.log('token: ' + token);
        console.log('tag: ' + t);
        console.log(t);
        return save(t);
      } else {
        return Rx.Observable.just(tag);
      }
    });
  }).concatMap(function (tag) {
    return fetch(tag); // is necessary for save, TODO move up save(tag).concatMap(tag -> fetch(tag));
  }).doOnNext(function (tag) {
    console.log(tag);
  }).toArray().filter(function (it) {
    return it.length > 0;
  }).concatMap(function (tags) {
    console.log(tags);
    for (var i = 0; i < tags.length; ++i) {
      console.log("add(): " + tags[i].hashtag);
      post.addUnique('tagList', tags[i]); // addUnique(key, item)
    }
    //post.addUnique('tagList', tags); // addUnique(key, array) has been tested, it's not expected
    console.log("add(): " + post);
    console.log(post);
    console.log(post.get('tagList'));
    return save(post);
  }) : Rx.Observable.empty();
}).doOnNext(function (post) {
  console.log('after:');
  console.log(post);
  console.log(post.get('taglist'));
  console.log('after: ' + post.get('tagList'));
  console.log('---');
}).subscribe(function (it) {}, function (e) {
  console.log(e);
});
//}).subscribe();

/**
 * Require `query.ascending('createdAt')` and did not set limit(), please use Rx.Observable.take() instead.
 * @param {Parse.Query} query
 */
function all(query) {
  var chunkSize = 100;
  return Rx.Observable.fromPromise(query.find()).concatMap(function (posts) {
    if (posts.length == chunkSize) {
      var q = query.greaterThan('createdAt', posts[posts.length - 1].get('createdAt'));
      return Rx.Observable.concat(Rx.Observable.from(posts), all(q));
    } else {
      return Rx.Observable.from(posts);
    }
  }).distinct(function (it) {
    return it.id;
  });
}

/**
 * @param {Parse.Object} parseObject
 */
function fetch(parseObject) {
  return Rx.Observable.fromPromise(parseObject.fetch()).map(function (it) {
    return parseObject;
  }).defaultIfEmpty(parseObject);
}

/**
 * @param {Parse.Object} parseObject
 */
function save(parseObject) {
  return Rx.Observable.fromPromise(parseObject.save()).map(function (it) {
    return parseObject;
  }).defaultIfEmpty(parseObject);
}

function testSaveTagIntoPost() {
  var q = new Parse.Query("Post");
  q.equalTo("objectId", "gpJQW0ZOgD");
  q.include("tagList");
  all(q).concatMap(function (post) {
    var q2 = new Parse.Query("Tag");
    q2.equalTo("objectId", "dCUztbH1jG");
    return all(q2).concatMap(function (tag) {
      console.log(post);
      console.log(post.get('tagList'));
      post.add('tagList', tag);
      console.log(post.get('tagList'));
      //return save(post);
      return Rx.Observable.fromPromise(post.save());
    });
  }).subscribe(function (it) {
    console.log(it);
  }, function (e) {
    console.log(e);
  });
}

function hasFile(f) {
  var fs = require('fs');
  var b = false;
  try {
    b = fs.statSync(f).isFile();
  } catch (e) {
  }
  return b;
}

/* vim: set sw=2: */
<commit_msg>comments<commit_after>#!/usr/bin/env node

var Parse = require('parse/node').Parse; // for parse
var Rx = require('rx'); // for rx
var configPath = process.env.HOME + '/.parse/' + 'config.json'; // $ docker-parse list 8tory_dev
var config = hasFile(configPath) ? require(configPath) : null;
var program = require('commander'); // for cli args
var Tag = Parse.Object.extend('Tag');
var Post = Parse.Object.extend('Post');

program
  .version('1.0.0')
  .option('-p, --production', 'production')
  .option('-a, --appId <appId>', 'appId')
  .option('-j, --jsKey <jsKey>', 'jsKey')
  .option('-m, --masterKey <masterKey>', 'masterKey')
  //.option('-d, --debug', 'Show debug messages')
  //.option('-i, --info', 'Show info only')
  //.option('-vv, --verbose', 'Show more messages')
  //.option('--years_ago <YEARS_AGO>', 'sync <YEARS_AGO> today 00:00:00 in user\'s timezone')
  //.option('--days <DAYS>', 'sync <DAYS> from <YEARS_AGO>')
  //.option('--since <SINCE>', 'A Unix timestamp or strtotime data value that points to the start of the range of time-based data')
  //.option('--until <UNTIL>', 'A Unix timestamp or strtotime data value that points to the end of the range of time-based data')
  //.option('--location', 'sync only location posts')
  //.option('-u, --user <USER>', 'User\'s object ID')
  .parse(process.argv);

var appId = program.appId ? program.appId : process.env.APP_ID;
var jsKey = program.jsKey ? program.jsKey : process.env.JS_KEY;
var masterKey = program.masterKey ? program.masterKey : process.env.MASTER_KEY;

console.log(hasFile(configPath));
if (config) {
  if (program.production) {
    console.warn('production');
    appId     = appId ? appId : config.production.appId;
    jsKey     = jsKey ? jsKey : config.production.jsKey;
    masterKey = masterKey ? masterKey : config.production.masterKey;
  } else {
    console.warn('dev');
    appId     = appId ? appId : config.dev.appId;
    jsKey     = jsKey ? jsKey : config.dev.jsKey;
    masterKey = masterKey ? masterKey : config.dev.masterKey;
  }
} else {
  console.warn('missing ' + configPath);
}

// TODO fatal error and exit
if (!appId) console.error('missing appId');
if (!jsKey) console.error('missing jsKey');
if (!masterKey) console.error('missing masterKey');

Parse.initialize(appId, jsKey, masterKey);

Parse.Cloud.useMasterKey();

// We should use message_tags from Facebook, not parse message field that's prefix with #
//
// ref.https://developers.facebook.com/docs/graph-api/reference/v2.6/post
//
// {
//   "message_tags": {
//     "{tag-key}": object,
//     "id": string,
//     "name": string,
//     "type": enum{user, page, group},
//     "offset": integer,
//     "length": integer,
//   }
// }
//

var query = new Parse.Query("Post");
//query.equalTo("isMainPost", true);
query.equalTo("source", "Fb");
query.notEqualTo("message", "");
query.ascending('createdAt'); // required for all(Parse.Query)
//var Post = Parse.Object.extend('Post');
query.include('tagList');

all(query).doOnNext(function(post) {
  console.log('before: ');
  console.log(post);
  console.log(post.get('message'));
  console.log(post.get('tagList'));
  //var tags = post.get('tagList');
  //for (var i = 0; i < tags.length; ++i) {
    //console.log(tags[i].hashtag);
  //}
}).concatMap(function (post) {
  var msg = post.get('message');
  if (!msg) return Rx.Observable.empty();
  var tokens = msg.match(/\#([^\u3000-\u303F。，\s]|[\w_-])+/g);
  return tokens ? Rx.Observable.from(tokens).concatMap(function (token) {
    var tagQuery = new Parse.Query('Tag');
    tagQuery.equalTo('hashtag', token);

    return Rx.Observable.fromPromise(tagQuery.first()).defaultIfEmpty(null).concatMap(function (tag) { // simulate switchIfEmpty(Rx.Observable)
      if (!tag) {
        var t = new Tag(); // for default
        t.set('type', 'hashtag');
        t.set('hashtag', token);
        console.log('token: ' + token);
        console.log('tag: ' + t);
        console.log(t);
        return save(t);
      } else {
        return Rx.Observable.just(tag);
      }
    });
  }).concatMap(function (tag) {
    return fetch(tag); // it's necessary after tag saving, TODO move up save(tag).concatMap(tag -> fetch(tag));
  }).doOnNext(function (tag) {
    console.log(tag);
  }).toArray().filter(function (it) {
    return it.length > 0;
  }).concatMap(function (tags) {
    console.log(tags);
    for (var i = 0; i < tags.length; ++i) {
      console.log("add(): " + tags[i].hashtag);
      post.addUnique('tagList', tags[i]); // addUnique(key, item)
    }
    //post.addUnique('tagList', tags); // addUnique(key, array) has been tested, it's not expected
    console.log("add(): " + post);
    console.log(post);
    console.log(post.get('tagList'));
    return save(post);
  }) : Rx.Observable.empty();
}).doOnNext(function (post) {
  console.log('after:');
  console.log(post);
  console.log(post.get('taglist'));
  console.log('after: ' + post.get('tagList'));
  console.log('---');
}).subscribe(function (it) {}, function (e) {
  console.log(e);
});
//}).subscribe();

/**
 * Require `query.ascending('createdAt')` and did not set limit(), please use Rx.Observable.take() instead.
 * @param {Parse.Query} query
 */
function all(query) {
  var chunkSize = 100;
  return Rx.Observable.fromPromise(query.find()).concatMap(function (posts) {
    if (posts.length == chunkSize) {
      var q = query.greaterThan('createdAt', posts[posts.length - 1].get('createdAt'));
      return Rx.Observable.concat(Rx.Observable.from(posts), all(q));
    } else {
      return Rx.Observable.from(posts);
    }
  }).distinct(function (it) {
    return it.id;
  });
}

/**
 * @param {Parse.Object} parseObject
 */
function fetch(parseObject) {
  return Rx.Observable.fromPromise(parseObject.fetch()).map(function (it) {
    return parseObject;
  }).defaultIfEmpty(parseObject);
}

/**
 * @param {Parse.Object} parseObject
 */
function save(parseObject) {
  return Rx.Observable.fromPromise(parseObject.save()).map(function (it) {
    return parseObject;
  }).defaultIfEmpty(parseObject);
}

function testSaveTagIntoPost() {
  var q = new Parse.Query("Post");
  q.equalTo("objectId", "gpJQW0ZOgD");
  q.include("tagList");
  all(q).concatMap(function (post) {
    var q2 = new Parse.Query("Tag");
    q2.equalTo("objectId", "dCUztbH1jG");
    return all(q2).concatMap(function (tag) {
      console.log(post);
      console.log(post.get('tagList'));
      post.add('tagList', tag);
      console.log(post.get('tagList'));
      //return save(post);
      return Rx.Observable.fromPromise(post.save());
    });
  }).subscribe(function (it) {
    console.log(it);
  }, function (e) {
    console.log(e);
  });
}

function hasFile(f) {
  var fs = require('fs');
  var b = false;
  try {
    b = fs.statSync(f).isFile();
  } catch (e) {
  }
  return b;
}

/* vim: set sw=2: */
<|endoftext|>