tml-io

TML io support for Javascript

npm install tml-io
8 downloads in the last month

tml-io(3) -- basic IO types and functions

SYNOPSIS

tml-io basic IO types and functions.

OPTIONS

SYNTAX

ENVIRONMENT

RETURN VALUES

STANDARDS

SECURITY CONSIDERATIONS

BUGS

HISTORY

AUTHOR

SEE ALSO

IMPLEMENTATION


Io

    @purpose Basic set of IO types and functions;
    @author Erin Phillips;
    @version 0.0.1-1;
    @history 2013-04-30 EMP [0.0.1-1] add-tml-build;

resources

    CON = console
    FS = fs
    ZLIB = zlib
    BUF = buffer
    TMLR = tml-runtime
    CORE = tml-core

locals

    log = CON::log
    getEnv = TMLR::getEnv

tags

notes

messages

    ERR-FILE {1} on file [{2}] failed;
    ERR-DIR {1} on directory [{2}] failed;
    ERR-NAME {1} name [{2}] invalid;
    ERR-ENV {1} not present in environment;
    DBG-FILECNT Directory [{1}] contains only {2} files like {3};

    ERR-TEST.100 {*} Values not matching. [{1} vs {2}];

code

-- STATICS -- ;

bus IoMain

event IoMain.statFile $ onFile& onNotFile&

    @usage Tests file state for file-ness, and performs onFile if
    fileName a file, and onNotFile otherwise;

    FS.stat(a,
      func e? opt stat%
        enforce nullval e ERR-FILE('Stat',a)
        if stat.isFile()
          onFile(a)
        else
          onNotFile(a)
        ;
    ;);

sensor readEnv $ orThrow!=false -> opt $

    =getEnv(a)
    if (noval b and orThrow)
        throw ERR-ENV(a);;

anvil makeFilePath %Dir ? -> %FilePath

    =case b $ new FilePath(con a.asString() b)
            %FileName
             new FilePath(con a.asString() b.asString());
    ;

-- TYPES -- ;

@need DIR ensure canonical rep with terminating '/'; @need DIR make filepaths from filenames;

type Dir(path$)

    @@set(path);

private method set $ -> this

    ensure-trailing a '/' @path=a;       

method asString -> $

    =@path;

method append $ -> this

    @@set(con @path a);

method makeSub $ -> %Dir

    =new Dir(con @path a);

method foreachFile sfx%FileSuffix onEach&2 onDone&

    @usage filters directory contents on suffix, performs onEach(err,f)
    for each file and onDone() afterward;

    FS.readdir(@path,
    func e? opt files*
        enforce nullval e ERR-DIR('Read',@path)
        parallel-each files as file batch 3
            if sfx.isMatching(file)
                fire IoMain.statFile(
                    con @path file
                    ,func $ onEach(a,next);
                    ,next)
            else
                next()
            ;
        finally
            onDone()
        ;
    ;);

pattern patGetPart :name :index

    <<<
    ###method get:name -> $
        coalesce= @parts @@parse(); a=@parts[:index];
    >>>

@need FILENAME cannot contain slashes;

type FileName(name$)

    enforce not-match name /[\&slash;]/ ERR-NAME('File',name)
    @name=name;

private method parse -> = scan @name /^(.?)([.][^.]*)?$/;

method asString -> $=@name;

render patGetPart Base 1;

render patGetPart Suffix 2;


type Utf8StreamConformer

    @partial=null
    @partialOld=null
    ;

method conform buf% -> ret%=buf

    vars partialLen len u8;

    if @partial
        ret = BUF::Buffer.concat([@partial,buf])
        unset @partial;

    ##IFUTFPARTIAL = <<<eq shift-right u8 :1 :2>>>

    len=ret.length

    u8=ret[sub len 1]

    if (##IFUTFPARTIAL 5 0x06;
        or
        ##IFUTFPARTIAL 4 0x0E;
        or
        ##IFUTFPARTIAL 3 0x1E;
        or
        ##IFUTFPARTIAL 2 0x3E;
        or
        ##IFUTFPARTIAL 1 0x7E;)
        partialLen=1
    else
        u8=ret[sub len 2]
        if (##IFUTFPARTIAL 4 0x0E;
            or
            ##IFUTFPARTIAL 3 0x1E;
            or
            ##IFUTFPARTIAL 2 0x3E;
            or
            ##IFUTFPARTIAL 1 0x7E;)
            partialLen=2
        else
            u8=ret[sub len 3]
            if (##IFUTFPARTIAL 3 0x1E;
                or
                ##IFUTFPARTIAL 2 0x3E;
                or
                ##IFUTFPARTIAL 1 0x7E;)
                partialLen=3
            else
                u8=ret[sub len 4]
                if (##IFUTFPARTIAL 2 0x3E;
                    or
                    ##IFUTFPARTIAL 1 0x7E;)
                    partialLen=4
                else
                    u8=ret[sub len 5]
                    if ##IFUTFPARTIAL 1 0x7E;
                        partialLen=5
                    ;
                ;
            ;
        ;
    ;

    if partialLen
        @partial = newex BUF::Buffer(partialLen)
        @purpose copy remaining u8s to temp buffer;
        ret.copy(@partial,0,sub len partialLen)
        ret=ret.slice(0,sub len partialLen)
        ;        

    ;

pattern patGetStat :opName :prop

    <<<
    ###method :opName &1
        if @stats
            a(@stats.:prop)
        else
            @@loadStats(
                func a(@stats.:prop););;
    >>>

type FilePath(@path$)

    initial state NEW -> LOADED;
    state LOADED ->;;

render patGetPart DirPath 1;

render patGetPart FileName 2;

render patGetPart FileBase 3;

render patGetPart Suffix 4;

render patGetStat isFile isFile();

render patGetStat isDir isDirectory();

render patGetStat size size;

render patGetStat lastModified mtime;

render patGetStat mode mode;

private method parse ->*

    =coalesce= @parts
        scan @path /^(.*?)(([^\&slash;]*?)([.][^.]*))?$/;;

private method loadStats cb&

    FS.stat(@path,
        func e? stats%
            enforce nullval e ERR-FILE('Stat',@path)
            @stats=stats
            cb()
        ;);

method asString -> $

    =@path;

method equals %FilePath -> !

    =(eq$ a.asString() @path);

method getText -> $

    =@text;

method isLoaded ->!

    =ami LOADED;

method isThere cb&1

    FS.exists(@path,cb);

method read cb&2

    FS.readFile(@path,
        {encoding:(coalesce @encoding 'utf8')}
        ,func opt e? opt text$
          if nullval e
            @text=text
            iam LOADED
          ;
          cb(e,text)
        ;)
    ;

method setEncoding $ -> this

    @encoding=a;

method setText $ -> this

    @text=a;

method streamReadBinary onData&1 onEnd&

    vars stream = FS.createReadStream(@path);
    if eq$ @getSuffix() '.gz'
        stream.pipe(ZLIB.createGunzip())
              .on('data',onData)
              .on('end', onEnd)
    else
        stream.on('data',onData)
              .on('end', onEnd);;

method streamReadUtf8 onData&1 onEnd&

    vars stream = FS.createReadStream(@path)
         utf8 = new Utf8StreamConformer()
         partial;

    if eq$ @getSuffix() '.gz'
        stream.pipe(ZLIB.createGunzip())
              .on('data',func % onData(utf8.conform(a).toString('utf8'));)
              .on('end', onEnd)
    else
        stream.on('data',func % onData(utf8.conform(a).toString('utf8'));)
              .on('end', onEnd);;
    FS.unlink(@path,cb);

method write cb&

    FS.writeFile(@path
        ,(coalesce @text '')
        ,{encoding:(coalesce @encoding 'utf8')}
        ,cb);

@need FILESUFFIX starts wth '.';

type FileSuffix(suffix$)

    @set(suffix);

method set $ -> this

    ensure-leading a '.'
    @suffix=a;

method asString -> $

    =@suffix;

method gen $ -> $

    =(con a @suffix);

method isMatching $ -> !

    =eq (scan a /^(.*?)([.][^.]*)?$/)[2] @suffix;

-- TESTS --;

test READENV

test-true : should return undefined for missing environment var

    noval readEnv('BOGUS')

test-throw : should throw on missing environment var

    readEnv('BOGUS',true);

    ;

test FILESUFFIX

test-eq : should not add leading period if already there

    (new FileSuffix('.hello')).asString() vs '.hello'

test-eq : should add leading period if not there

    (new FileSuffix('hello')).asString() vs '.hello'

test-eq : should generate filename with suffix

    (new FileSuffix('hello')).gen('ola') vs 'ola.hello'

test-true : should match suffix even if multiple suffixes

    (new FileSuffix('hello')).isMatching('this.that.hello')

test-false : should detect suffix mismatch when suffix present

    (new FileSuffix('hello')).isMatching('this.that.hellox')

test-false : should detect suffix mismatch when no suffix present

    (new FileSuffix('hello')).isMatching('hello')

    ;

test DIR

    vars o=new Dir('/etc')
         fileCount=0;

test-eq : should not append / if already present

    (new Dir('/tmp/')).asString() vs '/tmp/'

test-eq : should append / if already presnet

    (new Dir('/tmp')).asString() vs '/tmp/'

test-eq : should extend to a subdirectory

    (new Dir('/tmp/hello')).append('ola').asString() vs '/tmp/hello/ola/'

test-async : should apply for-each function to files in directory

    o.foreachFile(new FileSuffix('conf'),
        func f$ cb& inc fileCount cb();,
        func
          enforce gt fileCount 2 DBG-FILECNT(o.get(),fileCount,'*.conf')
          done()
        ;);

    ;

test FILENAME

    vars o=new FileName('hello.file.there');

test-throw : should reject a filename with leading /;

    new FileName('/this/there.fil');

test-eq : should return a filename base

    o.getBase() vs 'hello.file'

test-eq : should return a filename suffix

    o.getSuffix() vs '.there'

    ;

test FILEPATH

    vars f=new FilePath('/etc/passwd')
        f2=(new FilePath('/tmp/test.aa'))
                .setText('file test.aa')
        fp3=makeFilePath(
            (new Dir(getEnv('TML_HOME'))).append('etc')
            ,'gunzip-test.txt.gz')
        f3TestString="娱乐影评 - 武当休闲山庄\n"
        f3UncompressedString=''
        f4=new FilePath('/etc')
        ;

test-eq : should return a directory path

    f2.getDirPath() vs '/tmp/'

test-eq : should return a filename

    f2.getFileName() vs 'test.aa'

test-eq : should return a filename base

    f2.getFileBase() vs 'test'

test-eq : should return a filename suffix

    f2.getSuffix() vs '.aa'

test-async : should read the contents of a file

    f.read(
        func opt ? $
          enforce gt length b 0 'read /etc/passwd failed'
          done()
        ;);

test-async : should get the size of a file

    f.size(
        func #
          enforce gt a 0 'size /etc/passwd failed'
          done()
        ;);

test-async : should return true if a file

    f.isFile(
        func !
          enforce a 'isFile /etc/passwd failed'
          done()
        ;);

test-async : should return false if not a file

    f4.isFile(
        func !
          enforce not-true a 'isFile /etc failed'
          done()
        ;);

test-async : should return true if a directory

    f4.isDir(
        func !
          enforce a 'isDir /etc failed'
          done()
        ;);

test-async : should write, read, and delete a file

    f2.write(
        func e?
          enforce nullval e 'f2 test write failed'
          f2.isThere(func e?
            enforce e 'f2 file missing'
            f2.read(func e? text$
              enforce nullval e 'f2 test read failed'
              enforce eq$ text 'file test.aa' 'text not correct'
              f2.unlink(func e?
                enforce nullval e 'f2 test unlink failed'
                done()
              ;)
            ;)
          ;)
        ;);

test-async : should read a compressed file stream

    fp3.streamReadBinary(
        func ? con= f3UncompressedString a;
        ,func
            enforce eq$
                    f3UncompressedString
                    f3TestString
                    ERR-TEST.100(f3UncompressedString,f3TestString)
            done()
        ;);

test STREAM

vars fp=makeFilePath(
        (new Dir(getEnv('TML_HOME'))).append('etc')
        ,'utf8.txt')
     fpCheck=makeFilePath(
        (new Dir(getEnv('TML_HOME'))).append('etc')
        ,'utf8s.txt')
     text='';

test-async : should read a large UTF8 file stream

    fp.read(
        func opt ? fileText$
            fp.streamReadUtf8(
                func t$
                    con= text t;;
                ,func 
                    fpCheck.setText(text).write(
                        func 
                            enforce eq$ text fileText
                                'Whole file and parts do not match'
                            done()
                        ;)
                ;)
        ;);
    ;

    ;
npm loves you