initial commit
This commit is contained in:
commit
833af3b22e
|
@ -0,0 +1,3 @@
|
|||
bin
|
||||
pkg
|
||||
var
|
|
@ -0,0 +1,25 @@
|
|||
FROM phusion/baseimage:0.9.17
|
||||
|
||||
ENV RUNTIME=DOCKER
|
||||
ENV DATA_PATH=/home/scream/var
|
||||
|
||||
# Expose HTTP port
|
||||
EXPOSE 5000
|
||||
|
||||
# unelevated user
|
||||
RUN useradd --create-home scream
|
||||
|
||||
# Golang compilers
|
||||
RUN cd /usr/local && wget https://storage.googleapis.com/golang/go1.5.1.linux-amd64.tar.gz && \
|
||||
tar xf go1.5.1.linux-amd64.tar.gz && rm go1.5.1.linux-amd64.tar.gz
|
||||
|
||||
# install gb
|
||||
RUN mkdir /go && GOPATH=/go go get github.com/constabulary/gb/... \
|
||||
&& cp /go/bin/gb /usr/bin/gb
|
||||
|
||||
# Add application code and build
|
||||
ADD . /app
|
||||
RUN cd /app && gb build all
|
||||
|
||||
# Run
|
||||
CMD /sbin/my_init
|
|
@ -0,0 +1,21 @@
|
|||
scream
|
||||
======
|
||||
|
||||
A new core for Shuo's replacement.
|
||||
|
||||
build
|
||||
-----
|
||||
|
||||
```console
|
||||
$ gb build all
|
||||
```
|
||||
|
||||
test
|
||||
----
|
||||
|
||||
This uses [`gt`](https://godoc.org/rsc.io/gt).
|
||||
|
||||
```console
|
||||
$ go get rsc.io/gt
|
||||
$ ./test.sh
|
||||
```
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright (c) 2009 Thomas Jager. All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the following disclaimer
|
||||
// in the documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
// * Neither the name of Google Inc. nor the names of its
|
||||
// contributors may be used to endorse or promote products derived from
|
||||
// this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,65 @@
|
|||
Description
|
||||
-----------
|
||||
|
||||
Event based irc client library.
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
* Event based. Register Callbacks for the events you need to handle.
|
||||
* Handles basic irc demands for you
|
||||
* Standard CTCP
|
||||
* Reconnections on errors
|
||||
* Detect stoned servers
|
||||
|
||||
Install
|
||||
-------
|
||||
$ go get github.com/thoj/go-ircevent
|
||||
|
||||
Example
|
||||
-------
|
||||
See test/irc_test.go
|
||||
|
||||
Events for callbacks
|
||||
--------------------
|
||||
* 001 Welcome
|
||||
* PING
|
||||
* CTCP Unknown CTCP
|
||||
* CTCP_VERSION Version request (Handled internaly)
|
||||
* CTCP_USERINFO
|
||||
* CTCP_CLIENTINFO
|
||||
* CTCP_TIME
|
||||
* CTCP_PING
|
||||
* CTCP_ACTION (/me)
|
||||
* PRIVMSG
|
||||
* MODE
|
||||
* JOIN
|
||||
|
||||
+Many more
|
||||
|
||||
|
||||
AddCallback Example
|
||||
-------------------
|
||||
ircobj.AddCallback("PRIVMSG", func(event *irc.Event) {
|
||||
//event.Message() contains the message
|
||||
//event.Nick Contains the sender
|
||||
//event.Arguments[0] Contains the channel
|
||||
});
|
||||
|
||||
Commands
|
||||
--------
|
||||
ircobj := irc.IRC("<nick>", "<user>") //Create new ircobj
|
||||
//Set options
|
||||
ircobj.UseTLS = true //default is false
|
||||
//ircobj.TLSOptions //set ssl options
|
||||
ircobj.Password = "[server password]"
|
||||
//Commands
|
||||
ircobj.Connect("irc.someserver.com:6667") //Connect to server
|
||||
ircobj.SendRaw("<string>") //sends string to server. Adds \r\n
|
||||
ircobj.SendRawf("<formatstring>", ...) //sends formatted string to server.n
|
||||
ircobj.Join("<#channel> [password]")
|
||||
ircobj.Nick("newnick")
|
||||
ircobj.Privmsg("<nickname | #channel>", "msg")
|
||||
ircobj.Privmsgf(<nickname | #channel>, "<formatstring>", ...)
|
||||
ircobj.Notice("<nickname | #channel>", "msg")
|
||||
ircobj.Noticef("<nickname | #channel>", "<formatstring>", ...)
|
|
@ -0,0 +1,469 @@
|
|||
// Copyright 2009 Thomas Jager <mail@jager.no> All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
This package provides an event based IRC client library. It allows to
|
||||
register callbacks for the events you need to handle. Its features
|
||||
include handling standard CTCP, reconnecting on errors and detecting
|
||||
stones servers.
|
||||
Details of the IRC protocol can be found in the following RFCs:
|
||||
https://tools.ietf.org/html/rfc1459
|
||||
https://tools.ietf.org/html/rfc2810
|
||||
https://tools.ietf.org/html/rfc2811
|
||||
https://tools.ietf.org/html/rfc2812
|
||||
https://tools.ietf.org/html/rfc2813
|
||||
The details of the client-to-client protocol (CTCP) can be found here: http://www.irchelp.org/irchelp/rfc/ctcpspec.html
|
||||
*/
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
VERSION = "go-ircevent v2.1"
|
||||
)
|
||||
|
||||
var ErrDisconnected = errors.New("Disconnect Called")
|
||||
|
||||
// Read data from a connection. To be used as a goroutine.
|
||||
func (irc *Connection) readLoop() {
|
||||
defer irc.Done()
|
||||
br := bufio.NewReaderSize(irc.socket, 512)
|
||||
|
||||
errChan := irc.ErrorChan()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-irc.end:
|
||||
return
|
||||
default:
|
||||
// Set a read deadline based on the combined timeout and ping frequency
|
||||
// We should ALWAYS have received a response from the server within the timeout
|
||||
// after our own pings
|
||||
if irc.socket != nil {
|
||||
irc.socket.SetReadDeadline(time.Now().Add(irc.Timeout + irc.PingFreq))
|
||||
}
|
||||
|
||||
msg, err := br.ReadString('\n')
|
||||
|
||||
// We got past our blocking read, so bin timeout
|
||||
if irc.socket != nil {
|
||||
var zero time.Time
|
||||
irc.socket.SetReadDeadline(zero)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
break
|
||||
}
|
||||
|
||||
if irc.Debug {
|
||||
irc.Log.Printf("<-- %s\n", strings.TrimSpace(msg))
|
||||
}
|
||||
|
||||
irc.lastMessage = time.Now()
|
||||
event, err := parseToEvent(msg)
|
||||
event.Connection = irc
|
||||
if err == nil {
|
||||
/* XXX: len(args) == 0: args should be empty */
|
||||
irc.RunCallbacks(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//Parse raw irc messages
|
||||
func parseToEvent(msg string) (*Event, error) {
|
||||
msg = strings.TrimSuffix(msg, "\n") //Remove \r\n
|
||||
msg = strings.TrimSuffix(msg, "\r")
|
||||
event := &Event{Raw: msg}
|
||||
if len(msg) < 5 {
|
||||
return nil, errors.New("Malformed msg from server")
|
||||
}
|
||||
if msg[0] == ':' {
|
||||
if i := strings.Index(msg, " "); i > -1 {
|
||||
event.Source = msg[1:i]
|
||||
msg = msg[i+1 : len(msg)]
|
||||
|
||||
} else {
|
||||
return nil, errors.New("Malformed msg from server")
|
||||
}
|
||||
|
||||
if i, j := strings.Index(event.Source, "!"), strings.Index(event.Source, "@"); i > -1 && j > -1 && i < j {
|
||||
event.Nick = event.Source[0:i]
|
||||
event.User = event.Source[i+1 : j]
|
||||
event.Host = event.Source[j+1 : len(event.Source)]
|
||||
}
|
||||
}
|
||||
|
||||
split := strings.SplitN(msg, " :", 2)
|
||||
args := strings.Split(split[0], " ")
|
||||
event.Code = strings.ToUpper(args[0])
|
||||
event.Arguments = args[1:]
|
||||
if len(split) > 1 {
|
||||
event.Arguments = append(event.Arguments, split[1])
|
||||
}
|
||||
return event, nil
|
||||
|
||||
}
|
||||
|
||||
// Loop to write to a connection. To be used as a goroutine.
|
||||
func (irc *Connection) writeLoop() {
|
||||
defer irc.Done()
|
||||
errChan := irc.ErrorChan()
|
||||
for {
|
||||
select {
|
||||
case <-irc.end:
|
||||
return
|
||||
default:
|
||||
b, ok := <-irc.pwrite
|
||||
if !ok || b == "" || irc.socket == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if irc.Debug {
|
||||
irc.Log.Printf("--> %s\n", strings.TrimSpace(b))
|
||||
}
|
||||
|
||||
// Set a write deadline based on the time out
|
||||
irc.socket.SetWriteDeadline(time.Now().Add(irc.Timeout))
|
||||
|
||||
_, err := irc.socket.Write([]byte(b))
|
||||
|
||||
// Past blocking write, bin timeout
|
||||
var zero time.Time
|
||||
irc.socket.SetWriteDeadline(zero)
|
||||
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Pings the server if we have not received any messages for 5 minutes
|
||||
// to keep the connection alive. To be used as a goroutine.
|
||||
func (irc *Connection) pingLoop() {
|
||||
defer irc.Done()
|
||||
ticker := time.NewTicker(1 * time.Minute) // Tick every minute for monitoring
|
||||
ticker2 := time.NewTicker(irc.PingFreq) // Tick at the ping frequency.
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
//Ping if we haven't received anything from the server within the keep alive period
|
||||
if time.Since(irc.lastMessage) >= irc.KeepAlive {
|
||||
irc.SendRawf("PING %d", time.Now().UnixNano())
|
||||
}
|
||||
case <-ticker2.C:
|
||||
//Ping at the ping frequency
|
||||
irc.SendRawf("PING %d", time.Now().UnixNano())
|
||||
//Try to recapture nickname if it's not as configured.
|
||||
if irc.nick != irc.nickcurrent {
|
||||
irc.nickcurrent = irc.nick
|
||||
irc.SendRawf("NICK %s", irc.nick)
|
||||
}
|
||||
case <-irc.end:
|
||||
ticker.Stop()
|
||||
ticker2.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main loop to control the connection.
|
||||
func (irc *Connection) Loop() {
|
||||
errChan := irc.ErrorChan()
|
||||
for !irc.stopped {
|
||||
err := <-errChan
|
||||
if irc.stopped {
|
||||
break
|
||||
}
|
||||
irc.Log.Printf("Error, disconnected: %s\n", err)
|
||||
for !irc.stopped {
|
||||
if err = irc.Reconnect(); err != nil {
|
||||
irc.Log.Printf("Error while reconnecting: %s\n", err)
|
||||
time.Sleep(1 * time.Second)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Quit the current connection and disconnect from the server
|
||||
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1.6
|
||||
func (irc *Connection) Quit() {
|
||||
irc.SendRaw("QUIT")
|
||||
irc.stopped = true
|
||||
}
|
||||
|
||||
// Use the connection to join a given channel.
|
||||
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.1
|
||||
func (irc *Connection) Join(channel string) {
|
||||
irc.pwrite <- fmt.Sprintf("JOIN %s\r\n", channel)
|
||||
}
|
||||
|
||||
// Leave a given channel.
|
||||
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.2
|
||||
func (irc *Connection) Part(channel string) {
|
||||
irc.pwrite <- fmt.Sprintf("PART %s\r\n", channel)
|
||||
}
|
||||
|
||||
// Send a notification to a nickname. This is similar to Privmsg but must not receive replies.
|
||||
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.2
|
||||
func (irc *Connection) Notice(target, message string) {
|
||||
irc.pwrite <- fmt.Sprintf("NOTICE %s :%s\r\n", target, message)
|
||||
}
|
||||
|
||||
// Send a formated notification to a nickname.
|
||||
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.2
|
||||
func (irc *Connection) Noticef(target, format string, a ...interface{}) {
|
||||
irc.Notice(target, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// Send (action) message to a target (channel or nickname).
|
||||
// No clear RFC on this one...
|
||||
func (irc *Connection) Action(target, message string) {
|
||||
irc.pwrite <- fmt.Sprintf("PRIVMSG %s :\001ACTION %s\001\r\n", target, message)
|
||||
}
|
||||
|
||||
// Send formatted (action) message to a target (channel or nickname).
|
||||
func (irc *Connection) Actionf(target, format string, a ...interface{}) {
|
||||
irc.Action(target, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// Send (private) message to a target (channel or nickname).
|
||||
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.1
|
||||
func (irc *Connection) Privmsg(target, message string) {
|
||||
irc.pwrite <- fmt.Sprintf("PRIVMSG %s :%s\r\n", target, message)
|
||||
}
|
||||
|
||||
// Send formated string to specified target (channel or nickname).
|
||||
func (irc *Connection) Privmsgf(target, format string, a ...interface{}) {
|
||||
irc.Privmsg(target, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// Kick <user> from <channel> with <msg>. For no message, pass empty string ("")
|
||||
func (irc *Connection) Kick(user, channel, msg string) {
|
||||
var cmd bytes.Buffer
|
||||
cmd.WriteString(fmt.Sprintf("KICK %s %s", channel, user))
|
||||
if msg != "" {
|
||||
cmd.WriteString(fmt.Sprintf(" :%s", msg))
|
||||
}
|
||||
cmd.WriteString("\r\n")
|
||||
irc.pwrite <- cmd.String()
|
||||
}
|
||||
|
||||
// Kick all <users> from <channel> with <msg>. For no message, pass
|
||||
// empty string ("")
|
||||
func (irc *Connection) MultiKick(users []string, channel string, msg string) {
|
||||
var cmd bytes.Buffer
|
||||
cmd.WriteString(fmt.Sprintf("KICK %s %s", channel, strings.Join(users, ",")))
|
||||
if msg != "" {
|
||||
cmd.WriteString(fmt.Sprintf(" :%s", msg))
|
||||
}
|
||||
cmd.WriteString("\r\n")
|
||||
irc.pwrite <- cmd.String()
|
||||
}
|
||||
|
||||
// Send raw string.
|
||||
func (irc *Connection) SendRaw(message string) {
|
||||
irc.pwrite <- message + "\r\n"
|
||||
}
|
||||
|
||||
// Send raw formated string.
|
||||
func (irc *Connection) SendRawf(format string, a ...interface{}) {
|
||||
irc.SendRaw(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// Set (new) nickname.
|
||||
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1.2
|
||||
func (irc *Connection) Nick(n string) {
|
||||
irc.nick = n
|
||||
irc.SendRawf("NICK %s", n)
|
||||
}
|
||||
|
||||
// Determine nick currently used with the connection.
|
||||
func (irc *Connection) GetNick() string {
|
||||
return irc.nickcurrent
|
||||
}
|
||||
|
||||
// Query information about a particular nickname.
|
||||
// RFC 1459: https://tools.ietf.org/html/rfc1459#section-4.5.2
|
||||
func (irc *Connection) Whois(nick string) {
|
||||
irc.SendRawf("WHOIS %s", nick)
|
||||
}
|
||||
|
||||
// Query information about a given nickname in the server.
|
||||
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.5.1
|
||||
func (irc *Connection) Who(nick string) {
|
||||
irc.SendRawf("WHO %s", nick)
|
||||
}
|
||||
|
||||
// Set different modes for a target (channel or nickname).
|
||||
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.3
|
||||
func (irc *Connection) Mode(target string, modestring ...string) {
|
||||
if len(modestring) > 0 {
|
||||
mode := strings.Join(modestring, " ")
|
||||
irc.SendRawf("MODE %s %s", target, mode)
|
||||
return
|
||||
}
|
||||
irc.SendRawf("MODE %s", target)
|
||||
}
|
||||
|
||||
func (irc *Connection) ErrorChan() chan error {
|
||||
return irc.Error
|
||||
}
|
||||
|
||||
// Returns true if the connection is connected to an IRC server.
|
||||
func (irc *Connection) Connected() bool {
|
||||
return !irc.stopped
|
||||
}
|
||||
|
||||
// A disconnect sends all buffered messages (if possible),
|
||||
// stops all goroutines and then closes the socket.
|
||||
func (irc *Connection) Disconnect() {
|
||||
for event := range irc.events {
|
||||
irc.ClearCallback(event)
|
||||
}
|
||||
if irc.end != nil {
|
||||
close(irc.end)
|
||||
}
|
||||
|
||||
irc.end = nil
|
||||
|
||||
if irc.pwrite != nil {
|
||||
close(irc.pwrite)
|
||||
}
|
||||
|
||||
irc.Wait()
|
||||
if irc.socket != nil {
|
||||
irc.socket.Close()
|
||||
}
|
||||
irc.socket = nil
|
||||
irc.ErrorChan() <- ErrDisconnected
|
||||
}
|
||||
|
||||
// Reconnect to a server using the current connection.
|
||||
func (irc *Connection) Reconnect() error {
|
||||
if irc.end != nil {
|
||||
close(irc.end)
|
||||
}
|
||||
|
||||
irc.end = nil
|
||||
|
||||
irc.Wait() //make sure that wait group is cleared ensuring that all spawned goroutines have completed
|
||||
|
||||
irc.end = make(chan struct{})
|
||||
return irc.Connect(irc.Server)
|
||||
}
|
||||
|
||||
// Connect to a given server using the current connection configuration.
|
||||
// This function also takes care of identification if a password is provided.
|
||||
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1
|
||||
func (irc *Connection) Connect(server string) error {
|
||||
irc.Server = server
|
||||
// mark Server as stopped since there can be an error during connect
|
||||
irc.stopped = true
|
||||
|
||||
// make sure everything is ready for connection
|
||||
if len(irc.Server) == 0 {
|
||||
return errors.New("empty 'server'")
|
||||
}
|
||||
if strings.Count(irc.Server, ":") != 1 {
|
||||
return errors.New("wrong number of ':' in address")
|
||||
}
|
||||
if strings.Index(irc.Server, ":") == 0 {
|
||||
return errors.New("hostname is missing")
|
||||
}
|
||||
if strings.Index(irc.Server, ":") == len(irc.Server)-1 {
|
||||
return errors.New("port missing")
|
||||
}
|
||||
// check for valid range
|
||||
ports := strings.Split(irc.Server, ":")[1]
|
||||
port, err := strconv.Atoi(ports)
|
||||
if err != nil {
|
||||
return errors.New("extracting port failed")
|
||||
}
|
||||
if !((port >= 0) && (port <= 65535)) {
|
||||
return errors.New("port number outside valid range")
|
||||
}
|
||||
if irc.Log == nil {
|
||||
return errors.New("'Log' points to nil")
|
||||
}
|
||||
if len(irc.nick) == 0 {
|
||||
return errors.New("empty 'nick'")
|
||||
}
|
||||
if len(irc.user) == 0 {
|
||||
return errors.New("empty 'user'")
|
||||
}
|
||||
|
||||
if irc.UseTLS {
|
||||
dialer := &net.Dialer{Timeout: irc.Timeout}
|
||||
irc.socket, err = tls.DialWithDialer(dialer, "tcp", irc.Server, irc.TLSConfig)
|
||||
} else {
|
||||
irc.socket, err = net.DialTimeout("tcp", irc.Server, irc.Timeout)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
irc.stopped = false
|
||||
irc.Log.Printf("Connected to %s (%s)\n", irc.Server, irc.socket.RemoteAddr())
|
||||
|
||||
irc.pwrite = make(chan string, 10)
|
||||
irc.Error = make(chan error, 2)
|
||||
irc.Add(3)
|
||||
go irc.readLoop()
|
||||
go irc.writeLoop()
|
||||
go irc.pingLoop()
|
||||
if len(irc.Password) > 0 {
|
||||
irc.pwrite <- fmt.Sprintf("PASS %s\r\n", irc.Password)
|
||||
}
|
||||
irc.pwrite <- fmt.Sprintf("NICK %s\r\n", irc.nick)
|
||||
irc.pwrite <- fmt.Sprintf("USER %s 0.0.0.0 0.0.0.0 :%s\r\n", irc.user, irc.user)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a connection with the (publicly visible) nickname and username.
|
||||
// The nickname is later used to address the user. Returns nil if nick
|
||||
// or user are empty.
|
||||
func IRC(nick, user string) *Connection {
|
||||
// catch invalid values
|
||||
if len(nick) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(user) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
irc := &Connection{
|
||||
nick: nick,
|
||||
nickcurrent: nick,
|
||||
user: user,
|
||||
Log: log.New(os.Stdout, "", log.LstdFlags),
|
||||
end: make(chan struct{}),
|
||||
Version: VERSION,
|
||||
KeepAlive: 4 * time.Minute,
|
||||
Timeout: 1 * time.Minute,
|
||||
PingFreq: 15 * time.Minute,
|
||||
}
|
||||
irc.setupCallbacks()
|
||||
return irc
|
||||
}
|
|
@ -0,0 +1,229 @@
|
|||
package irc
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Register a callback to a connection and event code. A callback is a function
|
||||
// which takes only an Event pointer as parameter. Valid event codes are all
|
||||
// IRC/CTCP commands and error/response codes. This function returns the ID of
|
||||
// the registered callback for later management.
|
||||
func (irc *Connection) AddCallback(eventcode string, callback func(*Event)) string {
|
||||
eventcode = strings.ToUpper(eventcode)
|
||||
|
||||
if _, ok := irc.events[eventcode]; !ok {
|
||||
irc.events[eventcode] = make(map[string]func(*Event))
|
||||
}
|
||||
h := sha1.New()
|
||||
rawId := []byte(fmt.Sprintf("%v%d", reflect.ValueOf(callback).Pointer(), rand.Int63()))
|
||||
h.Write(rawId)
|
||||
id := fmt.Sprintf("%x", h.Sum(nil))
|
||||
irc.events[eventcode][id] = callback
|
||||
return id
|
||||
}
|
||||
|
||||
// Remove callback i (ID) from the given event code. This functions returns
|
||||
// true upon success, false if any error occurs.
|
||||
func (irc *Connection) RemoveCallback(eventcode string, i string) bool {
|
||||
eventcode = strings.ToUpper(eventcode)
|
||||
|
||||
if event, ok := irc.events[eventcode]; ok {
|
||||
if _, ok := event[i]; ok {
|
||||
delete(irc.events[eventcode], i)
|
||||
return true
|
||||
}
|
||||
irc.Log.Printf("Event found, but no callback found at id %s\n", i)
|
||||
return false
|
||||
}
|
||||
|
||||
irc.Log.Println("Event not found")
|
||||
return false
|
||||
}
|
||||
|
||||
// Remove all callbacks from a given event code. It returns true
|
||||
// if given event code is found and cleared.
|
||||
func (irc *Connection) ClearCallback(eventcode string) bool {
|
||||
eventcode = strings.ToUpper(eventcode)
|
||||
|
||||
if _, ok := irc.events[eventcode]; ok {
|
||||
irc.events[eventcode] = make(map[string]func(*Event))
|
||||
return true
|
||||
}
|
||||
|
||||
irc.Log.Println("Event not found")
|
||||
return false
|
||||
}
|
||||
|
||||
// Replace callback i (ID) associated with a given event code with a new callback function.
|
||||
func (irc *Connection) ReplaceCallback(eventcode string, i string, callback func(*Event)) {
|
||||
eventcode = strings.ToUpper(eventcode)
|
||||
|
||||
if event, ok := irc.events[eventcode]; ok {
|
||||
if _, ok := event[i]; ok {
|
||||
event[i] = callback
|
||||
return
|
||||
}
|
||||
irc.Log.Printf("Event found, but no callback found at id %s\n", i)
|
||||
}
|
||||
irc.Log.Printf("Event not found. Use AddCallBack\n")
|
||||
}
|
||||
|
||||
// Execute all callbacks associated with a given event.
|
||||
func (irc *Connection) RunCallbacks(event *Event) {
|
||||
msg := event.Message()
|
||||
if event.Code == "PRIVMSG" && len(msg) > 2 && msg[0] == '\x01' {
|
||||
event.Code = "CTCP" //Unknown CTCP
|
||||
|
||||
if i := strings.LastIndex(msg, "\x01"); i > 0 {
|
||||
msg = msg[1:i]
|
||||
} else {
|
||||
irc.Log.Printf("Invalid CTCP Message: %s\n", strconv.Quote(msg))
|
||||
return
|
||||
}
|
||||
|
||||
if msg == "VERSION" {
|
||||
event.Code = "CTCP_VERSION"
|
||||
|
||||
} else if msg == "TIME" {
|
||||
event.Code = "CTCP_TIME"
|
||||
|
||||
} else if strings.HasPrefix(msg, "PING") {
|
||||
event.Code = "CTCP_PING"
|
||||
|
||||
} else if msg == "USERINFO" {
|
||||
event.Code = "CTCP_USERINFO"
|
||||
|
||||
} else if msg == "CLIENTINFO" {
|
||||
event.Code = "CTCP_CLIENTINFO"
|
||||
|
||||
} else if strings.HasPrefix(msg, "ACTION") {
|
||||
event.Code = "CTCP_ACTION"
|
||||
if len(msg) > 6 {
|
||||
msg = msg[7:]
|
||||
} else {
|
||||
msg = ""
|
||||
}
|
||||
}
|
||||
|
||||
event.Arguments[len(event.Arguments)-1] = msg
|
||||
}
|
||||
|
||||
if callbacks, ok := irc.events[event.Code]; ok {
|
||||
if irc.VerboseCallbackHandler {
|
||||
irc.Log.Printf("%v (%v) >> %#v\n", event.Code, len(callbacks), event)
|
||||
}
|
||||
|
||||
for _, callback := range callbacks {
|
||||
go callback(event)
|
||||
}
|
||||
} else if irc.VerboseCallbackHandler {
|
||||
irc.Log.Printf("%v (0) >> %#v\n", event.Code, event)
|
||||
}
|
||||
|
||||
if callbacks, ok := irc.events["*"]; ok {
|
||||
if irc.VerboseCallbackHandler {
|
||||
irc.Log.Printf("Wildcard %v (%v) >> %#v\n", event.Code, len(callbacks), event)
|
||||
}
|
||||
|
||||
for _, callback := range callbacks {
|
||||
go callback(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set up some initial callbacks to handle the IRC/CTCP protocol.
|
||||
func (irc *Connection) setupCallbacks() {
|
||||
irc.events = make(map[string]map[string]func(*Event))
|
||||
|
||||
//Handle error events
|
||||
irc.AddCallback("ERROR", func(e *Event) { irc.Disconnect() })
|
||||
|
||||
//Handle ping events
|
||||
irc.AddCallback("PING", func(e *Event) { irc.SendRaw("PONG :" + e.Message()) })
|
||||
|
||||
//Version handler
|
||||
irc.AddCallback("CTCP_VERSION", func(e *Event) {
|
||||
irc.SendRawf("NOTICE %s :\x01VERSION %s\x01", e.Nick, irc.Version)
|
||||
})
|
||||
|
||||
irc.AddCallback("CTCP_USERINFO", func(e *Event) {
|
||||
irc.SendRawf("NOTICE %s :\x01USERINFO %s\x01", e.Nick, irc.user)
|
||||
})
|
||||
|
||||
irc.AddCallback("CTCP_CLIENTINFO", func(e *Event) {
|
||||
irc.SendRawf("NOTICE %s :\x01CLIENTINFO PING VERSION TIME USERINFO CLIENTINFO\x01", e.Nick)
|
||||
})
|
||||
|
||||
irc.AddCallback("CTCP_TIME", func(e *Event) {
|
||||
ltime := time.Now()
|
||||
irc.SendRawf("NOTICE %s :\x01TIME %s\x01", e.Nick, ltime.String())
|
||||
})
|
||||
|
||||
irc.AddCallback("CTCP_PING", func(e *Event) { irc.SendRawf("NOTICE %s :\x01%s\x01", e.Nick, e.Message()) })
|
||||
|
||||
// 437: ERR_UNAVAILRESOURCE "<nick/channel> :Nick/channel is temporarily unavailable"
|
||||
// Add a _ to current nick. If irc.nickcurrent is empty this cannot
|
||||
// work. It has to be set somewhere first in case the nick is already
|
||||
// taken or unavailable from the beginning.
|
||||
irc.AddCallback("437", func(e *Event) {
|
||||
// If irc.nickcurrent hasn't been set yet, set to irc.nick
|
||||
if irc.nickcurrent == "" {
|
||||
irc.nickcurrent = irc.nick
|
||||
}
|
||||
|
||||
if len(irc.nickcurrent) > 8 {
|
||||
irc.nickcurrent = "_" + irc.nickcurrent
|
||||
} else {
|
||||
irc.nickcurrent = irc.nickcurrent + "_"
|
||||
}
|
||||
irc.SendRawf("NICK %s", irc.nickcurrent)
|
||||
})
|
||||
|
||||
// 433: ERR_NICKNAMEINUSE "<nick> :Nickname is already in use"
|
||||
// Add a _ to current nick.
|
||||
irc.AddCallback("433", func(e *Event) {
|
||||
// If irc.nickcurrent hasn't been set yet, set to irc.nick
|
||||
if irc.nickcurrent == "" {
|
||||
irc.nickcurrent = irc.nick
|
||||
}
|
||||
|
||||
if len(irc.nickcurrent) > 8 {
|
||||
irc.nickcurrent = "_" + irc.nickcurrent
|
||||
} else {
|
||||
irc.nickcurrent = irc.nickcurrent + "_"
|
||||
}
|
||||
irc.SendRawf("NICK %s", irc.nickcurrent)
|
||||
})
|
||||
|
||||
irc.AddCallback("PONG", func(e *Event) {
|
||||
ns, _ := strconv.ParseInt(e.Message(), 10, 64)
|
||||
delta := time.Duration(time.Now().UnixNano() - ns)
|
||||
if irc.Debug {
|
||||
irc.Log.Printf("Lag: %vs\n", delta)
|
||||
}
|
||||
})
|
||||
|
||||
// NICK Define a nickname.
|
||||
// Set irc.nickcurrent to the new nick actually used in this connection.
|
||||
irc.AddCallback("NICK", func(e *Event) {
|
||||
if e.Nick == irc.nick {
|
||||
irc.nickcurrent = e.Message()
|
||||
}
|
||||
})
|
||||
|
||||
// 1: RPL_WELCOME "Welcome to the Internet Relay Network <nick>!<user>@<host>"
|
||||
// Set irc.nickcurrent to the actually used nick in this connection.
|
||||
irc.AddCallback("001", func(e *Event) {
|
||||
irc.nickcurrent = e.Arguments[0]
|
||||
})
|
||||
}
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright 2009 Thomas Jager <mail@jager.no> All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Connection struct {
|
||||
sync.WaitGroup
|
||||
Debug bool
|
||||
Error chan error
|
||||
Password string
|
||||
UseTLS bool
|
||||
TLSConfig *tls.Config
|
||||
Version string
|
||||
Timeout time.Duration
|
||||
PingFreq time.Duration
|
||||
KeepAlive time.Duration
|
||||
Server string
|
||||
|
||||
socket net.Conn
|
||||
pwrite chan string
|
||||
end chan struct{}
|
||||
|
||||
nick string //The nickname we want.
|
||||
nickcurrent string //The nickname we currently have.
|
||||
user string
|
||||
registered bool
|
||||
events map[string]map[string]func(*Event)
|
||||
|
||||
lastMessage time.Time
|
||||
|
||||
VerboseCallbackHandler bool
|
||||
Log *log.Logger
|
||||
|
||||
stopped bool
|
||||
}
|
||||
|
||||
// A struct to represent an event.
|
||||
type Event struct {
|
||||
Code string
|
||||
Raw string
|
||||
Nick string //<nick>
|
||||
Host string //<nick>!<usr>@<host>
|
||||
Source string //<host>
|
||||
User string //<usr>
|
||||
Arguments []string
|
||||
Connection *Connection
|
||||
}
|
||||
|
||||
// Retrieve the last message from Event arguments.
|
||||
// This function leaves the arguments untouched and
|
||||
// returns an empty string if there are none.
|
||||
func (e *Event) Message() string {
|
||||
if len(e.Arguments) == 0 {
|
||||
return ""
|
||||
}
|
||||
return e.Arguments[len(e.Arguments)-1]
|
||||
}
|
|
@ -0,0 +1,258 @@
|
|||
package irc
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConnectionEmtpyServer(t *testing.T) {
|
||||
irccon := IRC("go-eventirc", "go-eventirc")
|
||||
err := irccon.Connect("")
|
||||
if err == nil {
|
||||
t.Fatal("emtpy server string not detected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectionDoubleColon(t *testing.T) {
|
||||
irccon := IRC("go-eventirc", "go-eventirc")
|
||||
err := irccon.Connect("::")
|
||||
if err == nil {
|
||||
t.Fatal("wrong number of ':' not detected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectionMissingHost(t *testing.T) {
|
||||
irccon := IRC("go-eventirc", "go-eventirc")
|
||||
err := irccon.Connect(":6667")
|
||||
if err == nil {
|
||||
t.Fatal("missing host not detected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectionMissingPort(t *testing.T) {
|
||||
irccon := IRC("go-eventirc", "go-eventirc")
|
||||
err := irccon.Connect("chat.freenode.net:")
|
||||
if err == nil {
|
||||
t.Fatal("missing port not detected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectionNegativePort(t *testing.T) {
|
||||
irccon := IRC("go-eventirc", "go-eventirc")
|
||||
err := irccon.Connect("chat.freenode.net:-1")
|
||||
if err == nil {
|
||||
t.Fatal("negative port number not detected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectionTooLargePort(t *testing.T) {
|
||||
irccon := IRC("go-eventirc", "go-eventirc")
|
||||
err := irccon.Connect("chat.freenode.net:65536")
|
||||
if err == nil {
|
||||
t.Fatal("too large port number not detected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectionMissingLog(t *testing.T) {
|
||||
irccon := IRC("go-eventirc", "go-eventirc")
|
||||
irccon.Log = nil
|
||||
err := irccon.Connect("chat.freenode.net:6667")
|
||||
if err == nil {
|
||||
t.Fatal("missing 'Log' not detected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectionEmptyUser(t *testing.T) {
|
||||
irccon := IRC("go-eventirc", "go-eventirc")
|
||||
// user may be changed after creation
|
||||
irccon.user = ""
|
||||
err := irccon.Connect("chat.freenode.net:6667")
|
||||
if err == nil {
|
||||
t.Fatal("empty 'user' not detected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectionEmptyNick(t *testing.T) {
|
||||
irccon := IRC("go-eventirc", "go-eventirc")
|
||||
// nick may be changed after creation
|
||||
irccon.nick = ""
|
||||
err := irccon.Connect("chat.freenode.net:6667")
|
||||
if err == nil {
|
||||
t.Fatal("empty 'nick' not detected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveCallback(t *testing.T) {
|
||||
irccon := IRC("go-eventirc", "go-eventirc")
|
||||
irccon.VerboseCallbackHandler = true
|
||||
irccon.Debug = true
|
||||
|
||||
done := make(chan int, 10)
|
||||
|
||||
irccon.AddCallback("TEST", func(e *Event) { done <- 1 })
|
||||
id := irccon.AddCallback("TEST", func(e *Event) { done <- 2 })
|
||||
irccon.AddCallback("TEST", func(e *Event) { done <- 3 })
|
||||
|
||||
// Should remove callback at index 1
|
||||
irccon.RemoveCallback("TEST", id)
|
||||
|
||||
irccon.RunCallbacks(&Event{
|
||||
Code: "TEST",
|
||||
})
|
||||
|
||||
var results []int
|
||||
|
||||
results = append(results, <-done)
|
||||
results = append(results, <-done)
|
||||
|
||||
if len(results) != 2 || results[0] == 2 || results[1] == 2 {
|
||||
t.Error("Callback 2 not removed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWildcardCallback(t *testing.T) {
|
||||
irccon := IRC("go-eventirc", "go-eventirc")
|
||||
irccon.VerboseCallbackHandler = true
|
||||
irccon.Debug = true
|
||||
|
||||
done := make(chan int, 10)
|
||||
|
||||
irccon.AddCallback("TEST", func(e *Event) { done <- 1 })
|
||||
irccon.AddCallback("*", func(e *Event) { done <- 2 })
|
||||
|
||||
irccon.RunCallbacks(&Event{
|
||||
Code: "TEST",
|
||||
})
|
||||
|
||||
var results []int
|
||||
|
||||
results = append(results, <-done)
|
||||
results = append(results, <-done)
|
||||
|
||||
if len(results) != 2 || !(results[0] == 1 && results[1] == 2) {
|
||||
t.Error("Wildcard callback not called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearCallback(t *testing.T) {
|
||||
irccon := IRC("go-eventirc", "go-eventirc")
|
||||
irccon.VerboseCallbackHandler = true
|
||||
irccon.Debug = true
|
||||
|
||||
done := make(chan int, 10)
|
||||
|
||||
irccon.AddCallback("TEST", func(e *Event) { done <- 0 })
|
||||
irccon.AddCallback("TEST", func(e *Event) { done <- 1 })
|
||||
irccon.ClearCallback("TEST")
|
||||
irccon.AddCallback("TEST", func(e *Event) { done <- 2 })
|
||||
irccon.AddCallback("TEST", func(e *Event) { done <- 3 })
|
||||
|
||||
irccon.RunCallbacks(&Event{
|
||||
Code: "TEST",
|
||||
})
|
||||
|
||||
var results []int
|
||||
|
||||
results = append(results, <-done)
|
||||
results = append(results, <-done)
|
||||
|
||||
if len(results) != 2 || !(results[0] == 2 && results[1] == 3) {
|
||||
t.Error("Callbacks not cleared")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIRCemptyNick(t *testing.T) {
|
||||
irccon := IRC("", "go-eventirc")
|
||||
irccon = nil
|
||||
if irccon != nil {
|
||||
t.Error("empty nick didn't result in error")
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestIRCemptyUser(t *testing.T) {
|
||||
irccon := IRC("go-eventirc", "")
|
||||
if irccon != nil {
|
||||
t.Error("empty user didn't result in error")
|
||||
}
|
||||
}
|
||||
func TestConnection(t *testing.T) {
|
||||
irccon1 := IRC("go-eventirc1", "go-eventirc1")
|
||||
irccon1.VerboseCallbackHandler = true
|
||||
irccon1.Debug = true
|
||||
irccon2 := IRC("go-eventirc2", "go-eventirc2")
|
||||
irccon2.VerboseCallbackHandler = true
|
||||
irccon2.Debug = true
|
||||
err := irccon1.Connect("irc.freenode.net:6667")
|
||||
if err != nil {
|
||||
t.Log(err.Error())
|
||||
t.Fatal("Can't connect to freenode.")
|
||||
}
|
||||
err = irccon2.Connect("irc.freenode.net:6667")
|
||||
if err != nil {
|
||||
t.Log(err.Error())
|
||||
t.Fatal("Can't connect to freenode.")
|
||||
}
|
||||
irccon1.AddCallback("001", func(e *Event) { irccon1.Join("#go-eventirc") })
|
||||
irccon2.AddCallback("001", func(e *Event) { irccon2.Join("#go-eventirc") })
|
||||
con2ok := false
|
||||
irccon1.AddCallback("366", func(e *Event) {
|
||||
t := time.NewTicker(1 * time.Second)
|
||||
i := 10
|
||||
for {
|
||||
<-t.C
|
||||
irccon1.Privmsgf("#go-eventirc", "Test Message%d\n", i)
|
||||
if con2ok {
|
||||
i -= 1
|
||||
}
|
||||
if i == 0 {
|
||||
t.Stop()
|
||||
irccon1.Quit()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
irccon2.AddCallback("366", func(e *Event) {
|
||||
irccon2.Privmsg("#go-eventirc", "Test Message\n")
|
||||
con2ok = true
|
||||
irccon2.Nick("go-eventnewnick")
|
||||
})
|
||||
|
||||
irccon2.AddCallback("PRIVMSG", func(e *Event) {
|
||||
t.Log(e.Message())
|
||||
if e.Message() == "Test Message5" {
|
||||
irccon2.Quit()
|
||||
}
|
||||
})
|
||||
|
||||
irccon2.AddCallback("NICK", func(e *Event) {
|
||||
if irccon2.nickcurrent == "go-eventnewnick" {
|
||||
t.Fatal("Nick change did not work!")
|
||||
}
|
||||
})
|
||||
go irccon2.Loop()
|
||||
irccon1.Loop()
|
||||
}
|
||||
|
||||
func TestConnectionSSL(t *testing.T) {
|
||||
irccon := IRC("go-eventirc", "go-eventirc")
|
||||
irccon.VerboseCallbackHandler = true
|
||||
irccon.Debug = true
|
||||
irccon.UseTLS = true
|
||||
irccon.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
err := irccon.Connect("irc.freenode.net:7000")
|
||||
if err != nil {
|
||||
t.Log(err.Error())
|
||||
t.Fatal("Can't connect to freenode.")
|
||||
}
|
||||
irccon.AddCallback("001", func(e *Event) { irccon.Join("#go-eventirc") })
|
||||
|
||||
irccon.AddCallback("366", func(e *Event) {
|
||||
irccon.Privmsg("#go-eventirc", "Test Message\n")
|
||||
time.Sleep(2 * time.Second)
|
||||
irccon.Quit()
|
||||
})
|
||||
|
||||
irccon.Loop()
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
// +build gofuzz
|
||||
|
||||
package irc
|
||||
|
||||
func Fuzz(data []byte) int {
|
||||
b := bytes.NewBuffer(data)
|
||||
event, err := parseToEvent(b.String())
|
||||
if err == nil {
|
||||
irc := IRC("go-eventirc", "go-eventirc")
|
||||
irc.RunCallbacks(event)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"version": 0,
|
||||
"dependencies": [
|
||||
{
|
||||
"importpath": "github.com/Xe/uuid",
|
||||
"repository": "https://github.com/Xe/uuid",
|
||||
"revision": "62b230097e9c9534ca2074782b25d738c4b68964",
|
||||
"branch": "master"
|
||||
},
|
||||
{
|
||||
"importpath": "github.com/cjoudrey/gluahttp",
|
||||
"repository": "https://github.com/cjoudrey/gluahttp",
|
||||
"revision": "1128ce320b775e8e3fa2b8095b9c2116aa6869db",
|
||||
"branch": "master"
|
||||
},
|
||||
{
|
||||
"importpath": "github.com/layeh/gopher-json",
|
||||
"repository": "https://github.com/layeh/gopher-json",
|
||||
"revision": "bb1ff6467afab1f0ffee68113a256ce7435b578b",
|
||||
"branch": "master"
|
||||
},
|
||||
{
|
||||
"importpath": "github.com/layeh/gopher-luar",
|
||||
"repository": "https://github.com/layeh/gopher-luar",
|
||||
"revision": "ad06026c2b081cb9a2a563d3078f2ce67389d9fa",
|
||||
"branch": "master"
|
||||
},
|
||||
{
|
||||
"importpath": "github.com/robfig/cron",
|
||||
"repository": "https://github.com/robfig/cron",
|
||||
"revision": "67823cd24dece1b04cced3a0a0b3ca2bc84d875e",
|
||||
"branch": "master"
|
||||
},
|
||||
{
|
||||
"importpath": "github.com/scalingdata/gcfg",
|
||||
"repository": "https://github.com/scalingdata/gcfg",
|
||||
"revision": "37aabad69cfd3d20b8390d902a8b10e245c615ff",
|
||||
"branch": "master"
|
||||
},
|
||||
{
|
||||
"importpath": "github.com/yuin/gopher-lua",
|
||||
"repository": "https://github.com/yuin/gopher-lua",
|
||||
"revision": "21b70b48ba3b0da2122b79555094763ea97e3e98",
|
||||
"branch": "master"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
Paul Borman <borman@google.com>
|
||||
Christine Dodrill <xena@yolo-swag.com>
|
|
@ -0,0 +1,27 @@
|
|||
Copyright (c) 2009 Google Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,5 @@
|
|||
go-uuid
|
||||
=======
|
||||
|
||||
code.google.com is going away and I use this library a lot. It used to live at
|
||||
https://code.google.com/p/go-uuid/ but now I take care of it.
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright 2011 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// A Domain represents a Version 2 domain
|
||||
type Domain byte
|
||||
|
||||
// Domain constants for DCE Security (Version 2) UUIDs.
|
||||
const (
|
||||
Person = Domain(0)
|
||||
Group = Domain(1)
|
||||
Org = Domain(2)
|
||||
)
|
||||
|
||||
// NewDCESecurity returns a DCE Security (Version 2) UUID.
|
||||
//
|
||||
// The domain should be one of Person, Group or Org.
|
||||
// On a POSIX system the id should be the users UID for the Person
|
||||
// domain and the users GID for the Group. The meaning of id for
|
||||
// the domain Org or on non-POSIX systems is site defined.
|
||||
//
|
||||
// For a given domain/id pair the same token may be returned for up to
|
||||
// 7 minutes and 10 seconds.
|
||||
func NewDCESecurity(domain Domain, id uint32) UUID {
|
||||
uuid := NewUUID()
|
||||
if uuid != nil {
|
||||
uuid[6] = (uuid[6] & 0x0f) | 0x20 // Version 2
|
||||
uuid[9] = byte(domain)
|
||||
binary.BigEndian.PutUint32(uuid[0:], id)
|
||||
}
|
||||
return uuid
|
||||
}
|
||||
|
||||
// NewDCEPerson returns a DCE Security (Version 2) UUID in the person
|
||||
// domain with the id returned by os.Getuid.
|
||||
//
|
||||
// NewDCEPerson(Person, uint32(os.Getuid()))
|
||||
func NewDCEPerson() UUID {
|
||||
return NewDCESecurity(Person, uint32(os.Getuid()))
|
||||
}
|
||||
|
||||
// NewDCEGroup returns a DCE Security (Version 2) UUID in the group
|
||||
// domain with the id returned by os.Getgid.
|
||||
//
|
||||
// NewDCEGroup(Group, uint32(os.Getgid()))
|
||||
func NewDCEGroup() UUID {
|
||||
return NewDCESecurity(Group, uint32(os.Getgid()))
|
||||
}
|
||||
|
||||
// Domain returns the domain for a Version 2 UUID or false.
|
||||
func (uuid UUID) Domain() (Domain, bool) {
|
||||
if v, _ := uuid.Version(); v != 2 {
|
||||
return 0, false
|
||||
}
|
||||
return Domain(uuid[9]), true
|
||||
}
|
||||
|
||||
// Id returns the id for a Version 2 UUID or false.
|
||||
func (uuid UUID) Id() (uint32, bool) {
|
||||
if v, _ := uuid.Version(); v != 2 {
|
||||
return 0, false
|
||||
}
|
||||
return binary.BigEndian.Uint32(uuid[0:4]), true
|
||||
}
|
||||
|
||||
func (d Domain) String() string {
|
||||
switch d {
|
||||
case Person:
|
||||
return "Person"
|
||||
case Group:
|
||||
return "Group"
|
||||
case Org:
|
||||
return "Org"
|
||||
}
|
||||
return fmt.Sprintf("Domain%d", int(d))
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright 2011 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The uuid package generates and inspects UUIDs.
|
||||
//
|
||||
// UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security Services.
|
||||
package uuid
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright 2011 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"hash"
|
||||
)
|
||||
|
||||
// Well known Name Space IDs and UUIDs
|
||||
var (
|
||||
NameSpace_DNS = Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
|
||||
NameSpace_URL = Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8")
|
||||
NameSpace_OID = Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8")
|
||||
NameSpace_X500 = Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8")
|
||||
NIL = Parse("00000000-0000-0000-0000-000000000000")
|
||||
)
|
||||
|
||||
// NewHash returns a new UUID dervied from the hash of space concatenated with
|
||||
// data generated by h. The hash should be at least 16 byte in length. The
|
||||
// first 16 bytes of the hash are used to form the UUID. The version of the
|
||||
// UUID will be the lower 4 bits of version. NewHash is used to implement
|
||||
// NewMD5 and NewSHA1.
|
||||
func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID {
|
||||
h.Reset()
|
||||
h.Write(space)
|
||||
h.Write([]byte(data))
|
||||
s := h.Sum(nil)
|
||||
uuid := make([]byte, 16)
|
||||
copy(uuid, s)
|
||||
uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4)
|
||||
uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant
|
||||
return uuid
|
||||
}
|
||||
|
||||
// NewMD5 returns a new MD5 (Version 3) UUID based on the
|
||||
// supplied name space and data.
|
||||
//
|
||||
// NewHash(md5.New(), space, data, 3)
|
||||
func NewMD5(space UUID, data []byte) UUID {
|
||||
return NewHash(md5.New(), space, data, 3)
|
||||
}
|
||||
|
||||
// NewSHA1 returns a new SHA1 (Version 5) UUID based on the
|
||||
// supplied name space and data.
|
||||
//
|
||||
// NewHash(sha1.New(), space, data, 5)
|
||||
func NewSHA1(space UUID, data []byte) UUID {
|
||||
return NewHash(sha1.New(), space, data, 5)
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
// Copyright 2011 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import "net"
|
||||
|
||||
var (
|
||||
interfaces []net.Interface // cached list of interfaces
|
||||
ifname string // name of interface being used
|
||||
nodeID []byte // hardware for version 1 UUIDs
|
||||
)
|
||||
|
||||
// NodeInterface returns the name of the interface from which the NodeID was
|
||||
// derived. The interface "user" is returned if the NodeID was set by
|
||||
// SetNodeID.
|
||||
func NodeInterface() string {
|
||||
return ifname
|
||||
}
|
||||
|
||||
// SetNodeInterface selects the hardware address to be used for Version 1 UUIDs.
|
||||
// If name is "" then the first usable interface found will be used or a random
|
||||
// Node ID will be generated. If a named interface cannot be found then false
|
||||
// is returned.
|
||||
//
|
||||
// SetNodeInterface never fails when name is "".
|
||||
func SetNodeInterface(name string) bool {
|
||||
if interfaces == nil {
|
||||
var err error
|
||||
interfaces, err = net.Interfaces()
|
||||
if err != nil && name != "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for _, ifs := range interfaces {
|
||||
if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) {
|
||||
if setNodeID(ifs.HardwareAddr) {
|
||||
ifname = ifs.Name
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We found no interfaces with a valid hardware address. If name
|
||||
// does not specify a specific interface generate a random Node ID
|
||||
// (section 4.1.6)
|
||||
if name == "" {
|
||||
if nodeID == nil {
|
||||
nodeID = make([]byte, 6)
|
||||
}
|
||||
randomBits(nodeID)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NodeID returns a slice of a copy of the current Node ID, setting the Node ID
|
||||
// if not already set.
|
||||
func NodeID() []byte {
|
||||
if nodeID == nil {
|
||||
SetNodeInterface("")
|
||||
}
|
||||
nid := make([]byte, 6)
|
||||
copy(nid, nodeID)
|
||||
return nid
|
||||
}
|
||||
|
||||
// SetNodeID sets the Node ID to be used for Version 1 UUIDs. The first 6 bytes
|
||||
// of id are used. If id is less than 6 bytes then false is returned and the
|
||||
// Node ID is not set.
|
||||
func SetNodeID(id []byte) bool {
|
||||
if setNodeID(id) {
|
||||
ifname = "user"
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func setNodeID(id []byte) bool {
|
||||
if len(id) < 6 {
|
||||
return false
|
||||
}
|
||||
if nodeID == nil {
|
||||
nodeID = make([]byte, 6)
|
||||
}
|
||||
copy(nodeID, id)
|
||||
return true
|
||||
}
|
||||
|
||||
// NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is
|
||||
// not valid. The NodeID is only well defined for version 1 and 2 UUIDs.
|
||||
func (uuid UUID) NodeID() []byte {
|
||||
if len(uuid) != 16 {
|
||||
return nil
|
||||
}
|
||||
node := make([]byte, 6)
|
||||
copy(node, uuid[10:])
|
||||
return node
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
// Copyright 2014 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A Time represents a time as the number of 100's of nanoseconds since 15 Oct
|
||||
// 1582.
|
||||
type Time int64
|
||||
|
||||
const (
|
||||
lillian = 2299160 // Julian day of 15 Oct 1582
|
||||
unix = 2440587 // Julian day of 1 Jan 1970
|
||||
epoch = unix - lillian // Days between epochs
|
||||
g1582 = epoch * 86400 // seconds between epochs
|
||||
g1582ns100 = g1582 * 10000000 // 100s of a nanoseconds between epochs
|
||||
)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
lasttime uint64 // last time we returned
|
||||
clock_seq uint16 // clock sequence for this run
|
||||
|
||||
timeNow = time.Now // for testing
|
||||
)
|
||||
|
||||
// UnixTime converts t the number of seconds and nanoseconds using the Unix
|
||||
// epoch of 1 Jan 1970.
|
||||
func (t Time) UnixTime() (sec, nsec int64) {
|
||||
sec = int64(t - g1582ns100)
|
||||
nsec = (sec % 10000000) * 100
|
||||
sec /= 10000000
|
||||
return sec, nsec
|
||||
}
|
||||
|
||||
// GetTime returns the current Time (100s of nanoseconds since 15 Oct 1582) and
|
||||
// adjusts the clock sequence as needed. An error is returned if the current
|
||||
// time cannot be determined.
|
||||
func GetTime() (Time, error) {
|
||||
defer mu.Unlock()
|
||||
mu.Lock()
|
||||
return getTime()
|
||||
}
|
||||
|
||||
func getTime() (Time, error) {
|
||||
t := timeNow()
|
||||
|
||||
// If we don't have a clock sequence already, set one.
|
||||
if clock_seq == 0 {
|
||||
setClockSequence(-1)
|
||||
}
|
||||
now := uint64(t.UnixNano()/100) + g1582ns100
|
||||
|
||||
// If time has gone backwards with this clock sequence then we
|
||||
// increment the clock sequence
|
||||
if now <= lasttime {
|
||||
clock_seq = ((clock_seq + 1) & 0x3fff) | 0x8000
|
||||
}
|
||||
lasttime = now
|
||||
return Time(now), nil
|
||||
}
|
||||
|
||||
// ClockSequence returns the current clock sequence, generating one if not
|
||||
// already set. The clock sequence is only used for Version 1 UUIDs.
|
||||
//
|
||||
// The uuid package does not use global static storage for the clock sequence or
|
||||
// the last time a UUID was generated. Unless SetClockSequence a new random
|
||||
// clock sequence is generated the first time a clock sequence is requested by
|
||||
// ClockSequence, GetTime, or NewUUID. (section 4.2.1.1) sequence is generated
|
||||
// for
|
||||
func ClockSequence() int {
|
||||
defer mu.Unlock()
|
||||
mu.Lock()
|
||||
return clockSequence()
|
||||
}
|
||||
|
||||
func clockSequence() int {
|
||||
if clock_seq == 0 {
|
||||
setClockSequence(-1)
|
||||
}
|
||||
return int(clock_seq & 0x3fff)
|
||||
}
|
||||
|
||||
// SetClockSeq sets the clock sequence to the lower 14 bits of seq. Setting to
|
||||
// -1 causes a new sequence to be generated.
|
||||
func SetClockSequence(seq int) {
|
||||
defer mu.Unlock()
|
||||
mu.Lock()
|
||||
setClockSequence(seq)
|
||||
}
|
||||
|
||||
func setClockSequence(seq int) {
|
||||
if seq == -1 {
|
||||
var b [2]byte
|
||||
randomBits(b[:]) // clock sequence
|
||||
seq = int(b[0])<<8 | int(b[1])
|
||||
}
|
||||
old_seq := clock_seq
|
||||
clock_seq = uint16(seq&0x3fff) | 0x8000 // Set our variant
|
||||
if old_seq != clock_seq {
|
||||
lasttime = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in
|
||||
// uuid. It returns false if uuid is not valid. The time is only well defined
|
||||
// for version 1 and 2 UUIDs.
|
||||
func (uuid UUID) Time() (Time, bool) {
|
||||
if len(uuid) != 16 {
|
||||
return 0, false
|
||||
}
|
||||
time := int64(binary.BigEndian.Uint32(uuid[0:4]))
|
||||
time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32
|
||||
time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48
|
||||
return Time(time), true
|
||||
}
|
||||
|
||||
// ClockSequence returns the clock sequence encoded in uuid. It returns false
|
||||
// if uuid is not valid. The clock sequence is only well defined for version 1
|
||||
// and 2 UUIDs.
|
||||
func (uuid UUID) ClockSequence() (int, bool) {
|
||||
if len(uuid) != 16 {
|
||||
return 0, false
|
||||
}
|
||||
return int(binary.BigEndian.Uint16(uuid[8:10])) & 0x3fff, true
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright 2011 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// randomBits completely fills slice b with random data.
|
||||
func randomBits(b []byte) {
|
||||
if _, err := io.ReadFull(rander, b); err != nil {
|
||||
panic(err.Error()) // rand should never fail
|
||||
}
|
||||
}
|
||||
|
||||
// xvalues returns the value of a byte as a hexadecimal digit or 255.
|
||||
var xvalues = []byte{
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255,
|
||||
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
}
|
||||
|
||||
// xtob converts the the first two hex bytes of x into a byte.
|
||||
func xtob(x string) (byte, bool) {
|
||||
b1 := xvalues[x[0]]
|
||||
b2 := xvalues[x[1]]
|
||||
return (b1 << 4) | b2, b1 != 255 && b2 != 255
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
// Copyright 2011 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC
|
||||
// 4122.
|
||||
type UUID []byte
|
||||
|
||||
// A Version represents a UUIDs version.
|
||||
type Version byte
|
||||
|
||||
// A Variant represents a UUIDs variant.
|
||||
type Variant byte
|
||||
|
||||
// Constants returned by Variant.
|
||||
const (
|
||||
Invalid = Variant(iota) // Invalid UUID
|
||||
RFC4122 // The variant specified in RFC4122
|
||||
Reserved // Reserved, NCS backward compatibility.
|
||||
Microsoft // Reserved, Microsoft Corporation backward compatibility.
|
||||
Future // Reserved for future definition.
|
||||
)
|
||||
|
||||
var rander = rand.Reader // random function
|
||||
|
||||
// New returns a new random (version 4) UUID as a string. It is a convenience
|
||||
// function for NewRandom().String().
|
||||
func New() string {
|
||||
return NewRandom().String()
|
||||
}
|
||||
|
||||
// Parse decodes s into a UUID or returns nil. Both the UUID form of
|
||||
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and
|
||||
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded.
|
||||
func Parse(s string) UUID {
|
||||
if len(s) == 36+9 {
|
||||
if strings.ToLower(s[:9]) != "urn:uuid:" {
|
||||
return nil
|
||||
}
|
||||
s = s[9:]
|
||||
} else if len(s) != 36 {
|
||||
return nil
|
||||
}
|
||||
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
|
||||
return nil
|
||||
}
|
||||
uuid := make([]byte, 16)
|
||||
for i, x := range []int{
|
||||
0, 2, 4, 6,
|
||||
9, 11,
|
||||
14, 16,
|
||||
19, 21,
|
||||
24, 26, 28, 30, 32, 34} {
|
||||
if v, ok := xtob(s[x:]); !ok {
|
||||
return nil
|
||||
} else {
|
||||
uuid[i] = v
|
||||
}
|
||||
}
|
||||
return uuid
|
||||
}
|
||||
|
||||
// Equal returns true if uuid1 and uuid2 are equal.
|
||||
func Equal(uuid1, uuid2 UUID) bool {
|
||||
return bytes.Equal(uuid1, uuid2)
|
||||
}
|
||||
|
||||
// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
// , or "" if uuid is invalid.
|
||||
func (uuid UUID) String() string {
|
||||
if uuid == nil || len(uuid) != 16 {
|
||||
return ""
|
||||
}
|
||||
b := []byte(uuid)
|
||||
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
|
||||
b[:4], b[4:6], b[6:8], b[8:10], b[10:])
|
||||
}
|
||||
|
||||
// URN returns the RFC 2141 URN form of uuid,
|
||||
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, or "" if uuid is invalid.
|
||||
func (uuid UUID) URN() string {
|
||||
if uuid == nil || len(uuid) != 16 {
|
||||
return ""
|
||||
}
|
||||
b := []byte(uuid)
|
||||
return fmt.Sprintf("urn:uuid:%08x-%04x-%04x-%04x-%012x",
|
||||
b[:4], b[4:6], b[6:8], b[8:10], b[10:])
|
||||
}
|
||||
|
||||
// Variant returns the variant encoded in uuid. It returns Invalid if
|
||||
// uuid is invalid.
|
||||
func (uuid UUID) Variant() Variant {
|
||||
if len(uuid) != 16 {
|
||||
return Invalid
|
||||
}
|
||||
switch {
|
||||
case (uuid[8] & 0xc0) == 0x80:
|
||||
return RFC4122
|
||||
case (uuid[8] & 0xe0) == 0xc0:
|
||||
return Microsoft
|
||||
case (uuid[8] & 0xe0) == 0xe0:
|
||||
return Future
|
||||
default:
|
||||
return Reserved
|
||||
}
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
// Version returns the verison of uuid. It returns false if uuid is not
|
||||
// valid.
|
||||
func (uuid UUID) Version() (Version, bool) {
|
||||
if len(uuid) != 16 {
|
||||
return 0, false
|
||||
}
|
||||
return Version(uuid[6] >> 4), true
|
||||
}
|
||||
|
||||
func (v Version) String() string {
|
||||
if v > 15 {
|
||||
return fmt.Sprintf("BAD_VERSION_%d", v)
|
||||
}
|
||||
return fmt.Sprintf("VERSION_%d", v)
|
||||
}
|
||||
|
||||
func (v Variant) String() string {
|
||||
switch v {
|
||||
case RFC4122:
|
||||
return "RFC4122"
|
||||
case Reserved:
|
||||
return "Reserved"
|
||||
case Microsoft:
|
||||
return "Microsoft"
|
||||
case Future:
|
||||
return "Future"
|
||||
case Invalid:
|
||||
return "Invalid"
|
||||
}
|
||||
return fmt.Sprintf("BadVariant%d", int(v))
|
||||
}
|
||||
|
||||
// SetRand sets the random number generator to r, which implents io.Reader.
|
||||
// If r.Read returns an error when the package requests random data then
|
||||
// a panic will be issued.
|
||||
//
|
||||
// Calling SetRand with nil sets the random number generator to the default
|
||||
// generator.
|
||||
func SetRand(r io.Reader) {
|
||||
if r == nil {
|
||||
rander = rand.Reader
|
||||
return
|
||||
}
|
||||
rander = r
|
||||
}
|
|
@ -0,0 +1,390 @@
|
|||
// Copyright 2011 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type test struct {
|
||||
in string
|
||||
version Version
|
||||
variant Variant
|
||||
isuuid bool
|
||||
}
|
||||
|
||||
var tests = []test{
|
||||
{"f47ac10b-58cc-0372-8567-0e02b2c3d479", 0, RFC4122, true},
|
||||
{"f47ac10b-58cc-1372-8567-0e02b2c3d479", 1, RFC4122, true},
|
||||
{"f47ac10b-58cc-2372-8567-0e02b2c3d479", 2, RFC4122, true},
|
||||
{"f47ac10b-58cc-3372-8567-0e02b2c3d479", 3, RFC4122, true},
|
||||
{"f47ac10b-58cc-4372-8567-0e02b2c3d479", 4, RFC4122, true},
|
||||
{"f47ac10b-58cc-5372-8567-0e02b2c3d479", 5, RFC4122, true},
|
||||
{"f47ac10b-58cc-6372-8567-0e02b2c3d479", 6, RFC4122, true},
|
||||
{"f47ac10b-58cc-7372-8567-0e02b2c3d479", 7, RFC4122, true},
|
||||
{"f47ac10b-58cc-8372-8567-0e02b2c3d479", 8, RFC4122, true},
|
||||
{"f47ac10b-58cc-9372-8567-0e02b2c3d479", 9, RFC4122, true},
|
||||
{"f47ac10b-58cc-a372-8567-0e02b2c3d479", 10, RFC4122, true},
|
||||
{"f47ac10b-58cc-b372-8567-0e02b2c3d479", 11, RFC4122, true},
|
||||
{"f47ac10b-58cc-c372-8567-0e02b2c3d479", 12, RFC4122, true},
|
||||
{"f47ac10b-58cc-d372-8567-0e02b2c3d479", 13, RFC4122, true},
|
||||
{"f47ac10b-58cc-e372-8567-0e02b2c3d479", 14, RFC4122, true},
|
||||
{"f47ac10b-58cc-f372-8567-0e02b2c3d479", 15, RFC4122, true},
|
||||
|
||||
{"urn:uuid:f47ac10b-58cc-4372-0567-0e02b2c3d479", 4, Reserved, true},
|
||||
{"URN:UUID:f47ac10b-58cc-4372-0567-0e02b2c3d479", 4, Reserved, true},
|
||||
{"f47ac10b-58cc-4372-0567-0e02b2c3d479", 4, Reserved, true},
|
||||
{"f47ac10b-58cc-4372-1567-0e02b2c3d479", 4, Reserved, true},
|
||||
{"f47ac10b-58cc-4372-2567-0e02b2c3d479", 4, Reserved, true},
|
||||
{"f47ac10b-58cc-4372-3567-0e02b2c3d479", 4, Reserved, true},
|
||||
{"f47ac10b-58cc-4372-4567-0e02b2c3d479", 4, Reserved, true},
|
||||
{"f47ac10b-58cc-4372-5567-0e02b2c3d479", 4, Reserved, true},
|
||||
{"f47ac10b-58cc-4372-6567-0e02b2c3d479", 4, Reserved, true},
|
||||
{"f47ac10b-58cc-4372-7567-0e02b2c3d479", 4, Reserved, true},
|
||||
{"f47ac10b-58cc-4372-8567-0e02b2c3d479", 4, RFC4122, true},
|
||||
{"f47ac10b-58cc-4372-9567-0e02b2c3d479", 4, RFC4122, true},
|
||||
{"f47ac10b-58cc-4372-a567-0e02b2c3d479", 4, RFC4122, true},
|
||||
{"f47ac10b-58cc-4372-b567-0e02b2c3d479", 4, RFC4122, true},
|
||||
{"f47ac10b-58cc-4372-c567-0e02b2c3d479", 4, Microsoft, true},
|
||||
{"f47ac10b-58cc-4372-d567-0e02b2c3d479", 4, Microsoft, true},
|
||||
{"f47ac10b-58cc-4372-e567-0e02b2c3d479", 4, Future, true},
|
||||
{"f47ac10b-58cc-4372-f567-0e02b2c3d479", 4, Future, true},
|
||||
|
||||
{"f47ac10b158cc-5372-a567-0e02b2c3d479", 0, Invalid, false},
|
||||
{"f47ac10b-58cc25372-a567-0e02b2c3d479", 0, Invalid, false},
|
||||
{"f47ac10b-58cc-53723a567-0e02b2c3d479", 0, Invalid, false},
|
||||
{"f47ac10b-58cc-5372-a56740e02b2c3d479", 0, Invalid, false},
|
||||
{"f47ac10b-58cc-5372-a567-0e02-2c3d479", 0, Invalid, false},
|
||||
{"g47ac10b-58cc-4372-a567-0e02b2c3d479", 0, Invalid, false},
|
||||
}
|
||||
|
||||
var constants = []struct {
|
||||
c interface{}
|
||||
name string
|
||||
}{
|
||||
{Person, "Person"},
|
||||
{Group, "Group"},
|
||||
{Org, "Org"},
|
||||
{Invalid, "Invalid"},
|
||||
{RFC4122, "RFC4122"},
|
||||
{Reserved, "Reserved"},
|
||||
{Microsoft, "Microsoft"},
|
||||
{Future, "Future"},
|
||||
{Domain(17), "Domain17"},
|
||||
{Variant(42), "BadVariant42"},
|
||||
}
|
||||
|
||||
func testTest(t *testing.T, in string, tt test) {
|
||||
uuid := Parse(in)
|
||||
if ok := (uuid != nil); ok != tt.isuuid {
|
||||
t.Errorf("Parse(%s) got %v expected %v\b", in, ok, tt.isuuid)
|
||||
}
|
||||
if uuid == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if v := uuid.Variant(); v != tt.variant {
|
||||
t.Errorf("Variant(%s) got %d expected %d\b", in, v, tt.variant)
|
||||
}
|
||||
if v, _ := uuid.Version(); v != tt.version {
|
||||
t.Errorf("Version(%s) got %d expected %d\b", in, v, tt.version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUUID(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
testTest(t, tt.in, tt)
|
||||
testTest(t, strings.ToUpper(tt.in), tt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstants(t *testing.T) {
|
||||
for x, tt := range constants {
|
||||
v, ok := tt.c.(fmt.Stringer)
|
||||
if !ok {
|
||||
t.Errorf("%x: %v: not a stringer", x, v)
|
||||
} else if s := v.String(); s != tt.name {
|
||||
v, _ := tt.c.(int)
|
||||
t.Errorf("%x: Constant %T:%d gives %q, expected %q\n", x, tt.c, v, s, tt.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomUUID(t *testing.T) {
|
||||
m := make(map[string]bool)
|
||||
for x := 1; x < 32; x++ {
|
||||
uuid := NewRandom()
|
||||
s := uuid.String()
|
||||
if m[s] {
|
||||
t.Errorf("NewRandom returned duplicated UUID %s\n", s)
|
||||
}
|
||||
m[s] = true
|
||||
if v, _ := uuid.Version(); v != 4 {
|
||||
t.Errorf("Random UUID of version %s\n", v)
|
||||
}
|
||||
if uuid.Variant() != RFC4122 {
|
||||
t.Errorf("Random UUID is variant %d\n", uuid.Variant())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
m := make(map[string]bool)
|
||||
for x := 1; x < 32; x++ {
|
||||
s := New()
|
||||
if m[s] {
|
||||
t.Errorf("New returned duplicated UUID %s\n", s)
|
||||
}
|
||||
m[s] = true
|
||||
uuid := Parse(s)
|
||||
if uuid == nil {
|
||||
t.Errorf("New returned %q which does not decode\n", s)
|
||||
continue
|
||||
}
|
||||
if v, _ := uuid.Version(); v != 4 {
|
||||
t.Errorf("Random UUID of version %s\n", v)
|
||||
}
|
||||
if uuid.Variant() != RFC4122 {
|
||||
t.Errorf("Random UUID is variant %d\n", uuid.Variant())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func clockSeq(t *testing.T, uuid UUID) int {
|
||||
seq, ok := uuid.ClockSequence()
|
||||
if !ok {
|
||||
t.Fatalf("%s: invalid clock sequence\n", uuid)
|
||||
}
|
||||
return seq
|
||||
}
|
||||
|
||||
func TestClockSeq(t *testing.T) {
|
||||
// Fake time.Now for this test to return a monotonically advancing time; restore it at end.
|
||||
defer func(orig func() time.Time) { timeNow = orig }(timeNow)
|
||||
monTime := time.Now()
|
||||
timeNow = func() time.Time {
|
||||
monTime = monTime.Add(1 * time.Second)
|
||||
return monTime
|
||||
}
|
||||
|
||||
SetClockSequence(-1)
|
||||
uuid1 := NewUUID()
|
||||
uuid2 := NewUUID()
|
||||
|
||||
if clockSeq(t, uuid1) != clockSeq(t, uuid2) {
|
||||
t.Errorf("clock sequence %d != %d\n", clockSeq(t, uuid1), clockSeq(t, uuid2))
|
||||
}
|
||||
|
||||
SetClockSequence(-1)
|
||||
uuid2 = NewUUID()
|
||||
|
||||
// Just on the very off chance we generated the same sequence
|
||||
// two times we try again.
|
||||
if clockSeq(t, uuid1) == clockSeq(t, uuid2) {
|
||||
SetClockSequence(-1)
|
||||
uuid2 = NewUUID()
|
||||
}
|
||||
if clockSeq(t, uuid1) == clockSeq(t, uuid2) {
|
||||
t.Errorf("Duplicate clock sequence %d\n", clockSeq(t, uuid1))
|
||||
}
|
||||
|
||||
SetClockSequence(0x1234)
|
||||
uuid1 = NewUUID()
|
||||
if seq := clockSeq(t, uuid1); seq != 0x1234 {
|
||||
t.Errorf("%s: expected seq 0x1234 got 0x%04x\n", uuid1, seq)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoding(t *testing.T) {
|
||||
text := "7d444840-9dc0-11d1-b245-5ffdce74fad2"
|
||||
urn := "urn:uuid:7d444840-9dc0-11d1-b245-5ffdce74fad2"
|
||||
data := UUID{
|
||||
0x7d, 0x44, 0x48, 0x40,
|
||||
0x9d, 0xc0,
|
||||
0x11, 0xd1,
|
||||
0xb2, 0x45,
|
||||
0x5f, 0xfd, 0xce, 0x74, 0xfa, 0xd2,
|
||||
}
|
||||
if v := data.String(); v != text {
|
||||
t.Errorf("%x: encoded to %s, expected %s\n", data, v, text)
|
||||
}
|
||||
if v := data.URN(); v != urn {
|
||||
t.Errorf("%x: urn is %s, expected %s\n", data, v, urn)
|
||||
}
|
||||
|
||||
uuid := Parse(text)
|
||||
if !Equal(uuid, data) {
|
||||
t.Errorf("%s: decoded to %s, expected %s\n", text, uuid, data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersion1(t *testing.T) {
|
||||
uuid1 := NewUUID()
|
||||
uuid2 := NewUUID()
|
||||
|
||||
if Equal(uuid1, uuid2) {
|
||||
t.Errorf("%s:duplicate uuid\n", uuid1)
|
||||
}
|
||||
if v, _ := uuid1.Version(); v != 1 {
|
||||
t.Errorf("%s: version %s expected 1\n", uuid1, v)
|
||||
}
|
||||
if v, _ := uuid2.Version(); v != 1 {
|
||||
t.Errorf("%s: version %s expected 1\n", uuid2, v)
|
||||
}
|
||||
n1 := uuid1.NodeID()
|
||||
n2 := uuid2.NodeID()
|
||||
if !bytes.Equal(n1, n2) {
|
||||
t.Errorf("Different nodes %x != %x\n", n1, n2)
|
||||
}
|
||||
t1, ok := uuid1.Time()
|
||||
if !ok {
|
||||
t.Errorf("%s: invalid time\n", uuid1)
|
||||
}
|
||||
t2, ok := uuid2.Time()
|
||||
if !ok {
|
||||
t.Errorf("%s: invalid time\n", uuid2)
|
||||
}
|
||||
q1, ok := uuid1.ClockSequence()
|
||||
if !ok {
|
||||
t.Errorf("%s: invalid clock sequence\n", uuid1)
|
||||
}
|
||||
q2, ok := uuid2.ClockSequence()
|
||||
if !ok {
|
||||
t.Errorf("%s: invalid clock sequence", uuid2)
|
||||
}
|
||||
|
||||
switch {
|
||||
case t1 == t2 && q1 == q2:
|
||||
t.Errorf("time stopped\n")
|
||||
case t1 > t2 && q1 == q2:
|
||||
t.Errorf("time reversed\n")
|
||||
case t1 < t2 && q1 != q2:
|
||||
t.Errorf("clock sequence chaned unexpectedly\n")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeAndTime(t *testing.T) {
|
||||
// Time is February 5, 1998 12:30:23.136364800 AM GMT
|
||||
|
||||
uuid := Parse("7d444840-9dc0-11d1-b245-5ffdce74fad2")
|
||||
node := []byte{0x5f, 0xfd, 0xce, 0x74, 0xfa, 0xd2}
|
||||
|
||||
ts, ok := uuid.Time()
|
||||
if ok {
|
||||
c := time.Unix(ts.UnixTime())
|
||||
want := time.Date(1998, 2, 5, 0, 30, 23, 136364800, time.UTC)
|
||||
if !c.Equal(want) {
|
||||
t.Errorf("Got time %v, want %v", c, want)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("%s: bad time\n", uuid)
|
||||
}
|
||||
if !bytes.Equal(node, uuid.NodeID()) {
|
||||
t.Errorf("Expected node %v got %v\n", node, uuid.NodeID())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMD5(t *testing.T) {
|
||||
uuid := NewMD5(NameSpace_DNS, []byte("python.org")).String()
|
||||
want := "6fa459ea-ee8a-3ca4-894e-db77e160355e"
|
||||
if uuid != want {
|
||||
t.Errorf("MD5: got %q expected %q\n", uuid, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSHA1(t *testing.T) {
|
||||
uuid := NewSHA1(NameSpace_DNS, []byte("python.org")).String()
|
||||
want := "886313e1-3b8a-5372-9b90-0c9aee199e5d"
|
||||
if uuid != want {
|
||||
t.Errorf("SHA1: got %q expected %q\n", uuid, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeID(t *testing.T) {
|
||||
nid := []byte{1, 2, 3, 4, 5, 6}
|
||||
SetNodeInterface("")
|
||||
s := NodeInterface()
|
||||
if s == "" || s == "user" {
|
||||
t.Errorf("NodeInterface %q after SetInteface\n", s)
|
||||
}
|
||||
node1 := NodeID()
|
||||
if node1 == nil {
|
||||
t.Errorf("NodeID nil after SetNodeInterface\n", s)
|
||||
}
|
||||
SetNodeID(nid)
|
||||
s = NodeInterface()
|
||||
if s != "user" {
|
||||
t.Errorf("Expected NodeInterface %q got %q\n", "user", s)
|
||||
}
|
||||
node2 := NodeID()
|
||||
if node2 == nil {
|
||||
t.Errorf("NodeID nil after SetNodeID\n", s)
|
||||
}
|
||||
if bytes.Equal(node1, node2) {
|
||||
t.Errorf("NodeID not changed after SetNodeID\n", s)
|
||||
} else if !bytes.Equal(nid, node2) {
|
||||
t.Errorf("NodeID is %x, expected %x\n", node2, nid)
|
||||
}
|
||||
}
|
||||
|
||||
func testDCE(t *testing.T, name string, uuid UUID, domain Domain, id uint32) {
|
||||
if uuid == nil {
|
||||
t.Errorf("%s failed\n", name)
|
||||
return
|
||||
}
|
||||
if v, _ := uuid.Version(); v != 2 {
|
||||
t.Errorf("%s: %s: expected version 2, got %s\n", name, uuid, v)
|
||||
return
|
||||
}
|
||||
if v, ok := uuid.Domain(); !ok || v != domain {
|
||||
if !ok {
|
||||
t.Errorf("%s: %d: Domain failed\n", name, uuid)
|
||||
} else {
|
||||
t.Errorf("%s: %s: expected domain %d, got %d\n", name, uuid, domain, v)
|
||||
}
|
||||
}
|
||||
if v, ok := uuid.Id(); !ok || v != id {
|
||||
if !ok {
|
||||
t.Errorf("%s: %d: Id failed\n", name, uuid)
|
||||
} else {
|
||||
t.Errorf("%s: %s: expected id %d, got %d\n", name, uuid, id, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDCE(t *testing.T) {
|
||||
testDCE(t, "NewDCESecurity", NewDCESecurity(42, 12345678), 42, 12345678)
|
||||
testDCE(t, "NewDCEPerson", NewDCEPerson(), Person, uint32(os.Getuid()))
|
||||
testDCE(t, "NewDCEGroup", NewDCEGroup(), Group, uint32(os.Getgid()))
|
||||
}
|
||||
|
||||
type badRand struct{}
|
||||
|
||||
func (r badRand) Read(buf []byte) (int, error) {
|
||||
for i, _ := range buf {
|
||||
buf[i] = byte(i)
|
||||
}
|
||||
return len(buf), nil
|
||||
}
|
||||
|
||||
func TestBadRand(t *testing.T) {
|
||||
SetRand(badRand{})
|
||||
uuid1 := New()
|
||||
uuid2 := New()
|
||||
if uuid1 != uuid2 {
|
||||
t.Errorf("execpted duplicates, got %q and %q\n", uuid1, uuid2)
|
||||
}
|
||||
SetRand(nil)
|
||||
uuid1 = New()
|
||||
uuid2 = New()
|
||||
if uuid1 == uuid2 {
|
||||
t.Errorf("unexecpted duplicates, got %q\n", uuid1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright 2011 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
// NewUUID returns a Version 1 UUID based on the current NodeID and clock
|
||||
// sequence, and the current time. If the NodeID has not been set by SetNodeID
|
||||
// or SetNodeInterface then it will be set automatically. If the NodeID cannot
|
||||
// be set NewUUID returns nil. If clock sequence has not been set by
|
||||
// SetClockSequence then it will be set automatically. If GetTime fails to
|
||||
// return the current NewUUID returns nil.
|
||||
func NewUUID() UUID {
|
||||
if nodeID == nil {
|
||||
SetNodeInterface("")
|
||||
}
|
||||
|
||||
now, err := GetTime()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
uuid := make([]byte, 16)
|
||||
|
||||
time_low := uint32(now & 0xffffffff)
|
||||
time_mid := uint16((now >> 32) & 0xffff)
|
||||
time_hi := uint16((now >> 48) & 0x0fff)
|
||||
time_hi |= 0x1000 // Version 1
|
||||
|
||||
binary.BigEndian.PutUint32(uuid[0:], time_low)
|
||||
binary.BigEndian.PutUint16(uuid[4:], time_mid)
|
||||
binary.BigEndian.PutUint16(uuid[6:], time_hi)
|
||||
binary.BigEndian.PutUint16(uuid[8:], clock_seq)
|
||||
copy(uuid[10:], nodeID)
|
||||
|
||||
return uuid
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright 2011 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
// Random returns a Random (Version 4) UUID or panics.
|
||||
//
|
||||
// The strength of the UUIDs is based on the strength of the crypto/rand
|
||||
// package.
|
||||
//
|
||||
// A note about uniqueness derived from from the UUID Wikipedia entry:
|
||||
//
|
||||
// Randomly generated UUIDs have 122 random bits. One's annual risk of being
|
||||
// hit by a meteorite is estimated to be one chance in 17 billion, that
|
||||
// means the probability is about 0.00000000006 (6 × 10−11),
|
||||
// equivalent to the odds of creating a few tens of trillions of UUIDs in a
|
||||
// year and having one duplicate.
|
||||
func NewRandom() UUID {
|
||||
uuid := make([]byte, 16)
|
||||
randomBits([]byte(uuid))
|
||||
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
|
||||
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
|
||||
return uuid
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Christian Joudrey
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
# gluahttp
|
||||
|
||||
[![](https://travis-ci.org/cjoudrey/gluahttp.svg)](https://travis-ci.org/cjoudrey/gluahttp)
|
||||
|
||||
gluahttp provides an easy way to make HTTP requests from within [GopherLua](https://github.com/yuin/gopher-lua).
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
go get github.com/cjoudrey/gluahttp
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
import "github.com/yuin/gopher-lua"
|
||||
import "github.com/cjoudrey/gluahttp"
|
||||
|
||||
func main() {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
L.PreloadModule("http", NewHttpModule(&http.Client{}).Loader)
|
||||
|
||||
if err := L.DoString(`
|
||||
|
||||
local http = require("http")
|
||||
|
||||
response, error_message = http.request("GET", "http://example.com", {
|
||||
query="page=1"
|
||||
headers={
|
||||
Accept="*/*"
|
||||
}
|
||||
})
|
||||
|
||||
`); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
- [`http.delete(url [, options])`](#httpdeleteurl--options)
|
||||
- [`http.get(url [, options])`](#httpgeturl--options)
|
||||
- [`http.head(url [, options])`](#httpheadurl--options)
|
||||
- [`http.patch(url [, options])`](#httppatchurl--options)
|
||||
- [`http.post(url [, options])`](#httpposturl--options)
|
||||
- [`http.put(url [, options])`](#httpputurl--options)
|
||||
- [`http.request(method, url [, options])`](#httprequestmethod-url--options)
|
||||
- [`http.request_batch(requests)`](#httprequest_batchrequests)
|
||||
- [`http.response`](#httpresponse)
|
||||
|
||||
### http.delete(url [, options])
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| url | String | URL of the resource to load |
|
||||
| options | Table | Additional options |
|
||||
|
||||
**Options**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| query | String | URL encoded query params |
|
||||
| cookies | Table | Additional cookies to send with the request |
|
||||
| headers | Table | Additional headers to send with the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[http.response](#httpresponse) or (nil, error message)
|
||||
|
||||
### http.get(url [, options])
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| url | String | URL of the resource to load |
|
||||
| options | Table | Additional options |
|
||||
|
||||
**Options**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| query | String | URL encoded query params |
|
||||
| cookies | Table | Additional cookies to send with the request |
|
||||
| headers | Table | Additional headers to send with the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[http.response](#httpresponse) or (nil, error message)
|
||||
|
||||
### http.head(url [, options])
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| url | String | URL of the resource to load |
|
||||
| options | Table | Additional options |
|
||||
|
||||
**Options**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| query | String | URL encoded query params |
|
||||
| cookies | Table | Additional cookies to send with the request |
|
||||
| headers | Table | Additional headers to send with the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[http.response](#httpresponse) or (nil, error message)
|
||||
|
||||
### http.patch(url [, options])
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| url | String | URL of the resource to load |
|
||||
| options | Table | Additional options |
|
||||
|
||||
**Options**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| query | String | URL encoded query params |
|
||||
| cookies | Table | Additional cookies to send with the request |
|
||||
| form | String | URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` |
|
||||
| headers | Table | Additional headers to send with the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[http.response](#httpresponse) or (nil, error message)
|
||||
|
||||
### http.post(url [, options])
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| url | String | URL of the resource to load |
|
||||
| options | Table | Additional options |
|
||||
|
||||
**Options**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| query | String | URL encoded query params |
|
||||
| cookies | Table | Additional cookies to send with the request |
|
||||
| form | String | URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` |
|
||||
| headers | Table | Additional headers to send with the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[http.response](#httpresponse) or (nil, error message)
|
||||
|
||||
### http.put(url [, options])
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| url | String | URL of the resource to load |
|
||||
| options | Table | Additional options |
|
||||
|
||||
**Options**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| query | String | URL encoded query params |
|
||||
| cookies | Table | Additional cookies to send with the request |
|
||||
| form | String | URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` |
|
||||
| headers | Table | Additional headers to send with the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[http.response](#httpresponse) or (nil, error message)
|
||||
|
||||
### http.request(method, url [, options])
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| method | String | The HTTP request method |
|
||||
| url | String | URL of the resource to load |
|
||||
| options | Table | Additional options |
|
||||
|
||||
**Options**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ------ | ----------- |
|
||||
| query | String | URL encoded query params |
|
||||
| cookies | Table | Additional cookies to send with the request |
|
||||
| form | String | URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` |
|
||||
| headers | Table | Additional headers to send with the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[http.response](#httpresponse) or (nil, error message)
|
||||
|
||||
### http.request_batch(requests)
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| -------- | ----- | ----------- |
|
||||
| requests | Table | A table of requests to send. Each request item is by itself a table containing [http.request](#httprequestmethod-url--options) parameters for the request |
|
||||
|
||||
**Returns**
|
||||
|
||||
[[http.response](#httpresponse)] or ([[http.response](#httpresponse)], [error message])
|
||||
|
||||
### http.response
|
||||
|
||||
The `http.response` table contains information about a completed HTTP request.
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ----------- | ------ | ----------- |
|
||||
| body | String | The HTTP response body |
|
||||
| body_size | Number | The size of the HTTP reponse body in bytes |
|
||||
| headers | Table | The HTTP response headers |
|
||||
| cookies | Table | The cookies sent by the server in the HTTP response |
|
||||
| status_code | Number | The HTTP response status code |
|
||||
| url | String | The final URL the request ended pointing to after redirects |
|
|
@ -0,0 +1,214 @@
|
|||
package gluahttp
|
||||
|
||||
import "github.com/yuin/gopher-lua"
|
||||
import "net/http"
|
||||
import "fmt"
|
||||
import "errors"
|
||||
import "io"
|
||||
import "io/ioutil"
|
||||
import "strings"
|
||||
|
||||
type httpModule struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
type empty struct{}
|
||||
|
||||
func NewHttpModule(client *http.Client) *httpModule {
|
||||
return &httpModule{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *httpModule) Loader(L *lua.LState) int {
|
||||
mod := L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{
|
||||
"get": h.get,
|
||||
"delete": h.delete,
|
||||
"head": h.head,
|
||||
"patch": h.patch,
|
||||
"post": h.post,
|
||||
"put": h.put,
|
||||
"request": h.request,
|
||||
"request_batch": h.requestBatch,
|
||||
})
|
||||
registerHttpResponseType(mod, L)
|
||||
L.Push(mod)
|
||||
return 1
|
||||
}
|
||||
|
||||
func (h *httpModule) get(L *lua.LState) int {
|
||||
return h.doRequestAndPush(L, "get", L.ToString(1), L.ToTable(2))
|
||||
}
|
||||
|
||||
func (h *httpModule) delete(L *lua.LState) int {
|
||||
return h.doRequestAndPush(L, "delete", L.ToString(1), L.ToTable(2))
|
||||
}
|
||||
|
||||
func (h *httpModule) head(L *lua.LState) int {
|
||||
return h.doRequestAndPush(L, "head", L.ToString(1), L.ToTable(2))
|
||||
}
|
||||
|
||||
func (h *httpModule) patch(L *lua.LState) int {
|
||||
return h.doRequestAndPush(L, "patch", L.ToString(1), L.ToTable(2))
|
||||
}
|
||||
|
||||
func (h *httpModule) post(L *lua.LState) int {
|
||||
return h.doRequestAndPush(L, "post", L.ToString(1), L.ToTable(2))
|
||||
}
|
||||
|
||||
func (h *httpModule) put(L *lua.LState) int {
|
||||
return h.doRequestAndPush(L, "put", L.ToString(1), L.ToTable(2))
|
||||
}
|
||||
|
||||
func (h *httpModule) request(L *lua.LState) int {
|
||||
return h.doRequestAndPush(L, L.ToString(1), L.ToString(2), L.ToTable(3))
|
||||
}
|
||||
|
||||
func (h *httpModule) requestBatch(L *lua.LState) int {
|
||||
requests := L.ToTable(1)
|
||||
amountRequests := requests.Len()
|
||||
|
||||
errs := make([]error, amountRequests)
|
||||
responses := make([]*lua.LUserData, amountRequests)
|
||||
sem := make(chan empty, amountRequests)
|
||||
|
||||
i := 0
|
||||
|
||||
requests.ForEach(func(_ lua.LValue, value lua.LValue) {
|
||||
requestTable := toTable(value)
|
||||
|
||||
if requestTable != nil {
|
||||
method := requestTable.RawGet(lua.LNumber(1)).String()
|
||||
url := requestTable.RawGet(lua.LNumber(2)).String()
|
||||
options := toTable(requestTable.RawGet(lua.LNumber(3)))
|
||||
|
||||
go func(i int, L *lua.LState, method string, url string, options *lua.LTable) {
|
||||
response, err := h.doRequest(L, method, url, options)
|
||||
|
||||
if err == nil {
|
||||
errs[i] = nil
|
||||
responses[i] = response
|
||||
} else {
|
||||
errs[i] = err
|
||||
responses[i] = nil
|
||||
}
|
||||
|
||||
sem <- empty{}
|
||||
}(i, L, method, url, options)
|
||||
} else {
|
||||
errs[i] = errors.New("Request must be a table")
|
||||
responses[i] = nil
|
||||
sem <- empty{}
|
||||
}
|
||||
|
||||
i = i + 1
|
||||
})
|
||||
|
||||
for i = 0; i < amountRequests; i++ {
|
||||
<-sem
|
||||
}
|
||||
|
||||
hasErrors := false
|
||||
errorsTable := L.NewTable()
|
||||
responsesTable := L.NewTable()
|
||||
for i = 0; i < amountRequests; i++ {
|
||||
if errs[i] == nil {
|
||||
responsesTable.Append(responses[i])
|
||||
errorsTable.Append(lua.LNil)
|
||||
} else {
|
||||
responsesTable.Append(lua.LNil)
|
||||
errorsTable.Append(lua.LString(fmt.Sprintf("%s", errs[i])))
|
||||
hasErrors = true
|
||||
}
|
||||
}
|
||||
|
||||
if hasErrors {
|
||||
L.Push(responsesTable)
|
||||
L.Push(errorsTable)
|
||||
return 2
|
||||
} else {
|
||||
L.Push(responsesTable)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func (h *httpModule) doRequest(L *lua.LState, method string, url string, options *lua.LTable) (*lua.LUserData, error) {
|
||||
req, err := http.NewRequest(strings.ToUpper(method), url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if options != nil {
|
||||
if reqHeaders, ok := options.RawGet(lua.LString("headers")).(*lua.LTable); ok {
|
||||
reqHeaders.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
req.Header.Set(key.String(), value.String())
|
||||
})
|
||||
}
|
||||
|
||||
if reqCookies, ok := options.RawGet(lua.LString("cookies")).(*lua.LTable); ok {
|
||||
reqCookies.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
req.AddCookie(&http.Cookie{Name: key.String(), Value: value.String()})
|
||||
})
|
||||
}
|
||||
|
||||
switch reqQuery := options.RawGet(lua.LString("query")).(type) {
|
||||
case *lua.LNilType:
|
||||
break
|
||||
|
||||
case lua.LString:
|
||||
req.URL.RawQuery = reqQuery.String()
|
||||
break
|
||||
}
|
||||
|
||||
switch reqForm := options.RawGet(lua.LString("form")).(type) {
|
||||
case *lua.LNilType:
|
||||
break
|
||||
|
||||
case lua.LString:
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Body = ioutil.NopCloser(strings.NewReader(reqForm.String()))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
res, err := h.client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
if res != nil {
|
||||
io.Copy(ioutil.Discard, res.Body)
|
||||
defer res.Body.Close()
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: Add a way to discard body
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newHttpResponse(res, &body, len(body), L), nil
|
||||
}
|
||||
|
||||
func (h *httpModule) doRequestAndPush(L *lua.LState, method string, url string, options *lua.LTable) int {
|
||||
response, err := h.doRequest(L, method, url, options)
|
||||
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(fmt.Sprintf("%s", err)))
|
||||
return 2
|
||||
}
|
||||
|
||||
L.Push(response)
|
||||
return 1
|
||||
}
|
||||
|
||||
func toTable(v lua.LValue) *lua.LTable {
|
||||
if lv, ok := v.(*lua.LTable); ok {
|
||||
return lv
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,395 @@
|
|||
package gluahttp
|
||||
|
||||
import "github.com/yuin/gopher-lua"
|
||||
import "testing"
|
||||
import "io/ioutil"
|
||||
import "net/http"
|
||||
import "net"
|
||||
import "fmt"
|
||||
import "net/http/cookiejar"
|
||||
|
||||
func TestRequestNoMethod(t *testing.T) {
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.request()
|
||||
|
||||
assert_equal(nil, response)
|
||||
assert_equal('unsupported protocol scheme ""', error)
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestNoUrl(t *testing.T) {
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.request("get")
|
||||
|
||||
assert_equal(nil, response)
|
||||
assert_equal('Get : unsupported protocol scheme ""', error)
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestBatch(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
responses, errors = http.request_batch({
|
||||
{"get", "http://`+listener.Addr().String()+`", {query="page=1"}},
|
||||
{"post", "http://`+listener.Addr().String()+`/set_cookie"},
|
||||
{"post", ""},
|
||||
1
|
||||
})
|
||||
|
||||
assert_equal(nil, errors[1])
|
||||
assert_equal(nil, errors[2])
|
||||
assert_equal('Post : unsupported protocol scheme ""', errors[3])
|
||||
assert_equal('Request must be a table', errors[4])
|
||||
|
||||
assert_equal('Requested GET / with query "page=1"', responses[1]["body"])
|
||||
assert_equal('Cookie set!', responses[2]["body"])
|
||||
assert_equal('12345', responses[2]["cookies"]["session_id"])
|
||||
assert_equal(nil, responses[3])
|
||||
assert_equal(nil, responses[4])
|
||||
|
||||
responses, errors = http.request_batch({
|
||||
{"get", "http://`+listener.Addr().String()+`/get_cookie"}
|
||||
})
|
||||
|
||||
assert_equal(nil, errors)
|
||||
assert_equal("session_id=12345", responses[1]["body"])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestGet(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.request("get", "http://`+listener.Addr().String()+`")
|
||||
|
||||
assert_equal('Requested GET / with query ""', response['body'])
|
||||
assert_equal(200, response['status_code'])
|
||||
assert_equal('29', response['headers']['Content-Length'])
|
||||
assert_equal('text/plain; charset=utf-8', response['headers']['Content-Type'])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestGetWithRedirect(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.request("get", "http://`+listener.Addr().String()+`/redirect")
|
||||
|
||||
assert_equal('Requested GET / with query ""', response['body'])
|
||||
assert_equal(200, response['status_code'])
|
||||
assert_equal('http://`+listener.Addr().String()+`/', response['url'])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestPostForm(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.request("post", "http://`+listener.Addr().String()+`", {
|
||||
form="username=bob&password=secret"
|
||||
})
|
||||
|
||||
assert_equal(
|
||||
'Requested POST / with query ""' ..
|
||||
'Content-Type: application/x-www-form-urlencoded' ..
|
||||
'Body: username=bob&password=secret', response['body'])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestHeaders(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.request("post", "http://`+listener.Addr().String()+`", {
|
||||
headers={
|
||||
["Content-Type"]="application/json"
|
||||
}
|
||||
})
|
||||
|
||||
assert_equal(
|
||||
'Requested POST / with query ""' ..
|
||||
'Content-Type: application/json' ..
|
||||
'Body: ', response['body'])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestQuery(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.request("get", "http://`+listener.Addr().String()+`", {
|
||||
query="page=2"
|
||||
})
|
||||
|
||||
assert_equal('Requested GET / with query "page=2"', response['body'])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.get("http://`+listener.Addr().String()+`", {
|
||||
query="page=1"
|
||||
})
|
||||
|
||||
assert_equal('Requested GET / with query "page=1"', response['body'])
|
||||
assert_equal(200, response['status_code'])
|
||||
assert_equal('35', response['headers']['Content-Length'])
|
||||
assert_equal('text/plain; charset=utf-8', response['headers']['Content-Type'])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.delete("http://`+listener.Addr().String()+`", {
|
||||
query="page=1"
|
||||
})
|
||||
|
||||
assert_equal('Requested DELETE / with query "page=1"', response['body'])
|
||||
assert_equal(200, response['status_code'])
|
||||
assert_equal('38', response['headers']['Content-Length'])
|
||||
assert_equal('text/plain; charset=utf-8', response['headers']['Content-Type'])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHead(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.head("http://`+listener.Addr().String()+`/head", {
|
||||
query="page=1"
|
||||
})
|
||||
|
||||
assert_equal(200, response['status_code'])
|
||||
assert_equal("/head?page=1", response['headers']['X-Request-Uri'])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPost(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.post("http://`+listener.Addr().String()+`", {
|
||||
form="username=bob&password=secret"
|
||||
})
|
||||
|
||||
assert_equal(
|
||||
'Requested POST / with query ""' ..
|
||||
'Content-Type: application/x-www-form-urlencoded' ..
|
||||
'Body: username=bob&password=secret', response['body'])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatch(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.patch("http://`+listener.Addr().String()+`", {
|
||||
form="username=bob&password=secret"
|
||||
})
|
||||
|
||||
assert_equal(
|
||||
'Requested PATCH / with query ""' ..
|
||||
'Content-Type: application/x-www-form-urlencoded' ..
|
||||
'Body: username=bob&password=secret', response['body'])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPut(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.put("http://`+listener.Addr().String()+`", {
|
||||
form="username=bob&password=secret"
|
||||
})
|
||||
|
||||
assert_equal(
|
||||
'Requested PUT / with query ""' ..
|
||||
'Content-Type: application/x-www-form-urlencoded' ..
|
||||
'Body: username=bob&password=secret', response['body'])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseCookies(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.get("http://`+listener.Addr().String()+`/set_cookie")
|
||||
|
||||
assert_equal('Cookie set!', response["body"])
|
||||
assert_equal('12345', response["cookies"]["session_id"])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestCookies(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.get("http://`+listener.Addr().String()+`/get_cookie", {
|
||||
cookies={
|
||||
["session_id"]="test"
|
||||
}
|
||||
})
|
||||
|
||||
assert_equal('session_id=test', response["body"])
|
||||
assert_equal(15, response["body_size"])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseBodySize(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
response, error = http.get("http://`+listener.Addr().String()+`/")
|
||||
|
||||
assert_equal(29, response["body_size"])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseUrl(t *testing.T) {
|
||||
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
setupServer(listener)
|
||||
|
||||
if err := evalLua(t, `
|
||||
local http = require("http")
|
||||
|
||||
response, error = http.get("http://`+listener.Addr().String()+`/redirect")
|
||||
assert_equal("http://`+listener.Addr().String()+`/", response["url"])
|
||||
|
||||
response, error = http.get("http://`+listener.Addr().String()+`/get_cookie")
|
||||
assert_equal("http://`+listener.Addr().String()+`/get_cookie", response["url"])
|
||||
`); err != nil {
|
||||
t.Errorf("Failed to evaluate script: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func evalLua(t *testing.T, script string) error {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
cookieJar, _ := cookiejar.New(nil)
|
||||
|
||||
L.PreloadModule("http", NewHttpModule(&http.Client{
|
||||
Jar: cookieJar,
|
||||
},
|
||||
).Loader)
|
||||
|
||||
L.SetGlobal("assert_equal", L.NewFunction(func(L *lua.LState) int {
|
||||
expected := L.Get(1)
|
||||
actual := L.Get(2)
|
||||
|
||||
if expected.Type() != actual.Type() || expected.String() != actual.String() {
|
||||
t.Errorf("Expected %s %q, got %s %q", expected.Type(), expected, actual.Type(), actual)
|
||||
}
|
||||
|
||||
return 0
|
||||
}))
|
||||
|
||||
return L.DoString(script)
|
||||
}
|
||||
|
||||
func setupServer(listener net.Listener) {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
||||
fmt.Fprintf(w, "Requested %s / with query %q", req.Method, req.URL.RawQuery)
|
||||
|
||||
if req.Method == "POST" || req.Method == "PATCH" || req.Method == "PUT" {
|
||||
body, _ := ioutil.ReadAll(req.Body)
|
||||
fmt.Fprintf(w, "Content-Type: %s", req.Header.Get("Content-Type"))
|
||||
fmt.Fprintf(w, "Body: %s", body)
|
||||
}
|
||||
})
|
||||
mux.HandleFunc("/head", func(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "HEAD" {
|
||||
w.Header().Set("X-Request-Uri", req.URL.String())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
})
|
||||
mux.HandleFunc("/set_cookie", func(w http.ResponseWriter, req *http.Request) {
|
||||
http.SetCookie(w, &http.Cookie{Name: "session_id", Value: "12345"})
|
||||
fmt.Fprint(w, "Cookie set!")
|
||||
})
|
||||
mux.HandleFunc("/get_cookie", func(w http.ResponseWriter, req *http.Request) {
|
||||
session_id, _ := req.Cookie("session_id")
|
||||
fmt.Fprint(w, session_id)
|
||||
})
|
||||
mux.HandleFunc("/redirect", func(w http.ResponseWriter, req *http.Request) {
|
||||
http.Redirect(w, req, "/", http.StatusFound)
|
||||
})
|
||||
s := &http.Server{
|
||||
Handler: mux,
|
||||
}
|
||||
go s.Serve(listener)
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package gluahttp
|
||||
|
||||
import "github.com/yuin/gopher-lua"
|
||||
import "net/http"
|
||||
|
||||
const luaHttpResponseTypeName = "http.response"
|
||||
|
||||
type luaHttpResponse struct {
|
||||
res *http.Response
|
||||
body lua.LString
|
||||
bodySize int
|
||||
}
|
||||
|
||||
func registerHttpResponseType(module *lua.LTable, L *lua.LState) {
|
||||
mt := L.NewTypeMetatable(luaHttpResponseTypeName)
|
||||
L.SetField(mt, "__index", L.NewFunction(httpResponseIndex))
|
||||
|
||||
L.SetField(module, "response", mt)
|
||||
}
|
||||
|
||||
func newHttpResponse(res *http.Response, body *[]byte, bodySize int, L *lua.LState) *lua.LUserData {
|
||||
ud := L.NewUserData()
|
||||
ud.Value = &luaHttpResponse{
|
||||
res: res,
|
||||
body: lua.LString(*body),
|
||||
bodySize: bodySize,
|
||||
}
|
||||
L.SetMetatable(ud, L.GetTypeMetatable(luaHttpResponseTypeName))
|
||||
return ud
|
||||
}
|
||||
|
||||
func checkHttpResponse(L *lua.LState) *luaHttpResponse {
|
||||
ud := L.CheckUserData(1)
|
||||
if v, ok := ud.Value.(*luaHttpResponse); ok {
|
||||
return v
|
||||
}
|
||||
L.ArgError(1, "http.response expected")
|
||||
return nil
|
||||
}
|
||||
|
||||
func httpResponseIndex(L *lua.LState) int {
|
||||
res := checkHttpResponse(L)
|
||||
|
||||
switch L.CheckString(2) {
|
||||
case "headers":
|
||||
return httpResponseHeaders(res, L)
|
||||
case "cookies":
|
||||
return httpResponseCookies(res, L)
|
||||
case "status_code":
|
||||
return httpResponseStatusCode(res, L)
|
||||
case "url":
|
||||
return httpResponseUrl(res, L)
|
||||
case "body":
|
||||
return httpResponseBody(res, L)
|
||||
case "body_size":
|
||||
return httpResponseBodySize(res, L)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func httpResponseHeaders(res *luaHttpResponse, L *lua.LState) int {
|
||||
headers := L.NewTable()
|
||||
for key, _ := range res.res.Header {
|
||||
headers.RawSetString(key, lua.LString(res.res.Header.Get(key)))
|
||||
}
|
||||
L.Push(headers)
|
||||
return 1
|
||||
}
|
||||
|
||||
func httpResponseCookies(res *luaHttpResponse, L *lua.LState) int {
|
||||
cookies := L.NewTable()
|
||||
for _, cookie := range res.res.Cookies() {
|
||||
cookies.RawSetString(cookie.Name, lua.LString(cookie.Value))
|
||||
}
|
||||
L.Push(cookies)
|
||||
return 1
|
||||
}
|
||||
|
||||
func httpResponseStatusCode(res *luaHttpResponse, L *lua.LState) int {
|
||||
L.Push(lua.LNumber(res.res.StatusCode))
|
||||
return 1
|
||||
}
|
||||
|
||||
func httpResponseUrl(res *luaHttpResponse, L *lua.LState) int {
|
||||
L.Push(lua.LString(res.res.Request.URL.String()))
|
||||
return 1
|
||||
}
|
||||
|
||||
func httpResponseBody(res *luaHttpResponse, L *lua.LState) int {
|
||||
L.Push(&res.body)
|
||||
return 1
|
||||
}
|
||||
|
||||
func httpResponseBodySize(res *luaHttpResponse, L *lua.LState) int {
|
||||
L.Push(lua.LNumber(res.bodySize))
|
||||
return 1
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,7 @@
|
|||
# gopher-json [![GoDoc](https://godoc.org/github.com/layeh/gopher-json?status.svg)](https://godoc.org/github.com/layeh/gopher-json)
|
||||
|
||||
Package json is a simple JSON encoder/decoder for [gopher-lua](https://github.com/yuin/gopher-lua).
|
||||
|
||||
## License
|
||||
|
||||
Public domain.
|
|
@ -0,0 +1,40 @@
|
|||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
var api = map[string]lua.LGFunction{
|
||||
"decode": apiDecode,
|
||||
"encode": apiEncode,
|
||||
}
|
||||
|
||||
func apiDecode(L *lua.LState) int {
|
||||
str := L.CheckString(1)
|
||||
|
||||
var value interface{}
|
||||
err := json.Unmarshal([]byte(str), &value)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
L.Push(fromJSON(L, value))
|
||||
return 1
|
||||
}
|
||||
|
||||
func apiEncode(L *lua.LState) int {
|
||||
value := L.CheckAny(1)
|
||||
|
||||
visited := make(map[*lua.LTable]bool)
|
||||
data, err := toJSON(value, visited)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
L.Push(lua.LString(string(data)))
|
||||
return 1
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// Package json is a simple JSON encoder/decoder for gopher-lua.
|
||||
//
|
||||
// Documentation
|
||||
//
|
||||
// The following functions are exposed by the library:
|
||||
// decode(string): Decodes a JSON string. Returns nil and an error string if
|
||||
// the string could not be decoded.
|
||||
// encode(value): Encodes a value into a JSON string. Returns nil and an error
|
||||
// string if the value could not be encoded.
|
||||
//
|
||||
// Example
|
||||
//
|
||||
// Below is an example usage of the library:
|
||||
// L := lua.NewState()
|
||||
// luajson.Preload(s)
|
||||
package json
|
|
@ -0,0 +1,20 @@
|
|||
package json
|
||||
|
||||
import (
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// Preload adds json to the given Lua state's package.preload table. After it
|
||||
// has been preloaded, it can be loaded using require:
|
||||
//
|
||||
// local json = require("json")
|
||||
func Preload(L *lua.LState) {
|
||||
L.PreloadModule("json", load)
|
||||
}
|
||||
|
||||
func load(L *lua.LState) int {
|
||||
t := L.NewTable()
|
||||
L.SetFuncs(t, api)
|
||||
L.Push(t)
|
||||
return 1
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package json
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func TestSimple(t *testing.T) {
|
||||
const str = `
|
||||
local json = require("json")
|
||||
assert(type(json) == "table")
|
||||
assert(type(json.decode) == "function")
|
||||
assert(type(json.encode) == "function")
|
||||
|
||||
assert(json.encode(true) == "true")
|
||||
assert(json.encode(1) == "1")
|
||||
assert(json.encode(-10) == "-10")
|
||||
assert(json.encode(nil) == "{}")
|
||||
|
||||
local obj = {"a",1,"b",2,"c",3}
|
||||
local jsonStr = json.encode(obj)
|
||||
local jsonObj = json.decode(jsonStr)
|
||||
for i = 1, #obj do
|
||||
assert(obj[i] == jsonObj[i])
|
||||
end
|
||||
|
||||
local obj = {name="Tim",number=12345}
|
||||
local jsonStr = json.encode(obj)
|
||||
local jsonObj = json.decode(jsonStr)
|
||||
assert(obj.name == jsonObj.name)
|
||||
assert(obj.number == jsonObj.number)
|
||||
|
||||
local obj = {"a","b",what="c",[5]="asd"}
|
||||
local jsonStr = json.encode(obj)
|
||||
local jsonObj = json.decode(jsonStr)
|
||||
assert(obj[1] == jsonObj["1"])
|
||||
assert(obj[2] == jsonObj["2"])
|
||||
assert(obj.what == jsonObj["what"])
|
||||
assert(obj[5] == jsonObj["5"])
|
||||
|
||||
assert(json.decode("null") == nil)
|
||||
|
||||
assert(json.decode(json.encode({person={name = "tim",}})).person.name == "tim")
|
||||
|
||||
local obj = {
|
||||
abc = 123,
|
||||
def = nil,
|
||||
}
|
||||
local obj2 = {
|
||||
obj = obj,
|
||||
}
|
||||
obj.obj2 = obj2
|
||||
assert(json.encode(obj) == nil)
|
||||
`
|
||||
s := lua.NewState()
|
||||
Preload(s)
|
||||
if err := s.DoString(str); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
var (
|
||||
errFunction = errors.New("cannot encode function to JSON")
|
||||
errChannel = errors.New("cannot encode channel to JSON")
|
||||
errState = errors.New("cannot encode state to JSON")
|
||||
errUserData = errors.New("cannot encode userdata to JSON")
|
||||
errNested = errors.New("cannot encode recursively nested tables to JSON")
|
||||
)
|
||||
|
||||
type jsonValue struct {
|
||||
lua.LValue
|
||||
visited map[*lua.LTable]bool
|
||||
}
|
||||
|
||||
func (j jsonValue) MarshalJSON() ([]byte, error) {
|
||||
return toJSON(j.LValue, j.visited)
|
||||
}
|
||||
|
||||
func toJSON(value lua.LValue, visited map[*lua.LTable]bool) (data []byte, err error) {
|
||||
switch converted := value.(type) {
|
||||
case lua.LBool:
|
||||
data, err = json.Marshal(converted)
|
||||
case lua.LChannel:
|
||||
err = errChannel
|
||||
case lua.LNumber:
|
||||
data, err = json.Marshal(converted)
|
||||
case *lua.LFunction:
|
||||
err = errFunction
|
||||
case *lua.LNilType:
|
||||
data, err = json.Marshal(converted)
|
||||
case *lua.LState:
|
||||
err = errState
|
||||
case lua.LString:
|
||||
data, err = json.Marshal(converted)
|
||||
case *lua.LTable:
|
||||
var arr []jsonValue
|
||||
var obj map[string]jsonValue
|
||||
|
||||
if visited[converted] {
|
||||
panic(errNested)
|
||||
return // unreachable
|
||||
}
|
||||
visited[converted] = true
|
||||
|
||||
converted.ForEach(func(k lua.LValue, v lua.LValue) {
|
||||
i, numberKey := k.(lua.LNumber)
|
||||
if numberKey && obj == nil {
|
||||
index := int(i) - 1
|
||||
if index != len(arr) {
|
||||
// map out of order; convert to map
|
||||
obj = make(map[string]jsonValue)
|
||||
for i, value := range arr {
|
||||
obj[strconv.Itoa(i+1)] = value
|
||||
}
|
||||
obj[strconv.Itoa(index+1)] = jsonValue{v, visited}
|
||||
return
|
||||
}
|
||||
arr = append(arr, jsonValue{v, visited})
|
||||
return
|
||||
}
|
||||
if obj == nil {
|
||||
obj = make(map[string]jsonValue)
|
||||
for i, value := range arr {
|
||||
obj[strconv.Itoa(i+1)] = value
|
||||
}
|
||||
}
|
||||
obj[k.String()] = jsonValue{v, visited}
|
||||
})
|
||||
if obj != nil {
|
||||
data, err = json.Marshal(obj)
|
||||
} else {
|
||||
data, err = json.Marshal(arr)
|
||||
}
|
||||
case *lua.LUserData:
|
||||
// TODO: call metatable __tostring?
|
||||
err = errUserData
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func fromJSON(L *lua.LState, value interface{}) lua.LValue {
|
||||
switch converted := value.(type) {
|
||||
case bool:
|
||||
return lua.LBool(converted)
|
||||
case float64:
|
||||
return lua.LNumber(converted)
|
||||
case string:
|
||||
return lua.LString(converted)
|
||||
case []interface{}:
|
||||
arr := L.CreateTable(len(converted), 0)
|
||||
for _, item := range converted {
|
||||
arr.Append(fromJSON(L, item))
|
||||
}
|
||||
return arr
|
||||
case map[string]interface{}:
|
||||
tbl := L.CreateTable(0, len(converted))
|
||||
for key, item := range converted {
|
||||
tbl.RawSetH(lua.LString(key), fromJSON(L, item))
|
||||
}
|
||||
return tbl
|
||||
}
|
||||
return lua.LNil
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2015 Tim Cooper
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,7 @@
|
|||
# gopher-luar [![GoDoc](https://godoc.org/github.com/layeh/gopher-luar?status.svg)](https://godoc.org/github.com/layeh/gopher-luar)
|
||||
|
||||
custom type reflection for [gopher-lua](https://github.com/yuin/gopher-lua).
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
|
@ -0,0 +1,79 @@
|
|||
package luar
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func checkChan(L *lua.LState, idx int) reflect.Value {
|
||||
ud := L.CheckUserData(idx)
|
||||
ref := reflect.ValueOf(ud.Value)
|
||||
if ref.Kind() != reflect.Chan {
|
||||
L.ArgError(idx, "expecting chan")
|
||||
}
|
||||
return ref
|
||||
}
|
||||
|
||||
func chanIndex(L *lua.LState) int {
|
||||
_ = checkChan(L, 1)
|
||||
key := L.CheckString(2)
|
||||
|
||||
switch key {
|
||||
case "send":
|
||||
L.Push(L.NewFunction(chanSend))
|
||||
case "receive":
|
||||
L.Push(L.NewFunction(chanReceive))
|
||||
case "close":
|
||||
L.Push(L.NewFunction(chanClose))
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func chanLen(L *lua.LState) int {
|
||||
ref := checkChan(L, 1)
|
||||
L.Push(lua.LNumber(ref.Len()))
|
||||
return 1
|
||||
}
|
||||
|
||||
func chanEq(L *lua.LState) int {
|
||||
chan1 := checkChan(L, 1)
|
||||
chan2 := checkChan(L, 2)
|
||||
L.Push(lua.LBool(chan1 == chan2))
|
||||
return 1
|
||||
}
|
||||
|
||||
// chan methods
|
||||
|
||||
func chanSend(L *lua.LState) int {
|
||||
ref := checkChan(L, 1)
|
||||
value := L.CheckAny(2)
|
||||
convertedValue := lValueToReflect(value, ref.Type().Elem())
|
||||
if convertedValue.Type() != ref.Type().Elem() {
|
||||
L.ArgError(2, "incorrect type")
|
||||
}
|
||||
ref.Send(convertedValue)
|
||||
return 0
|
||||
}
|
||||
|
||||
func chanReceive(L *lua.LState) int {
|
||||
ref := checkChan(L, 1)
|
||||
|
||||
value, ok := ref.Recv()
|
||||
if !ok {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LBool(false))
|
||||
return 2
|
||||
}
|
||||
L.Push(New(L, value.Interface()))
|
||||
L.Push(lua.LBool(true))
|
||||
return 2
|
||||
}
|
||||
|
||||
func chanClose(L *lua.LState) int {
|
||||
ref := checkChan(L, 1)
|
||||
ref.Close()
|
||||
return 0
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
// Package luar provides custom type reflection to gopher-lua.
|
||||
//
|
||||
// Notice
|
||||
//
|
||||
// This package is currently in development, and its behavior may change. This
|
||||
// message will be removed once the package is considered stable.
|
||||
//
|
||||
// Basic types
|
||||
//
|
||||
// Go bool, number, and string types are converted to the equivalent basic
|
||||
// Lua type.
|
||||
//
|
||||
// Example:
|
||||
// New(L, "Hello World") -> lua.LString("Hello World")
|
||||
// New(L, uint(834)) -> lua.LNumber(uint(834))
|
||||
//
|
||||
// Channel types
|
||||
//
|
||||
// Channel types have the following methods defined:
|
||||
// receive(): Receives data from the channel. Returns nil plus false if the
|
||||
// channel is closed.
|
||||
// send(data): Sends data to the channel.
|
||||
// close(): Closes the channel.
|
||||
//
|
||||
// Taking the length (#) of a channel returns how many unread items are in its
|
||||
// buffer.
|
||||
//
|
||||
// Example:
|
||||
// ch := make(chan string)
|
||||
// L.SetGlobal("ch", New(L, ch))
|
||||
// ---
|
||||
// ch:receive() -- equivalent to v, ok := ch
|
||||
// ch:send("hello") -- equivalent to ch <- "hello"
|
||||
// ch:close() -- equivalent to close(ch)
|
||||
//
|
||||
// Function types
|
||||
//
|
||||
// Function types can be called from Lua. Its arguments and returned values
|
||||
// will be automatically converted from and to Lua types, respectively (see
|
||||
// exception below). However, a function that uses luar.LState can bypass the
|
||||
// automatic argument and return value conversion (see luar.LState
|
||||
// documentation for example).
|
||||
//
|
||||
// Example:
|
||||
// fn := func(name string, age uint) string {
|
||||
// return fmt.Sprintf("Hello %s, age %d", name, age)
|
||||
// }
|
||||
// L.SetGlobal("fn", New(L, fn))
|
||||
// ---
|
||||
// print(fn("Tim", 5)) -- prints "Hello Tim, age 5"
|
||||
//
|
||||
// A special conversion case happens when function returns a lua.LValue slice.
|
||||
// In that case, luar will automatically unpack the slice.
|
||||
//
|
||||
// Example:
|
||||
// fn := func() []lua.LValue {
|
||||
// return []lua.LValue{lua.LString("Hello"), lua.LNumber(2.5)}
|
||||
// }
|
||||
// L.SetGlobal("fn", New(L, fn))
|
||||
// ---
|
||||
// x, y = fn()
|
||||
// print(x) -- prints "Hello"
|
||||
// print(y) -- prints "2.5"
|
||||
//
|
||||
// Map types
|
||||
//
|
||||
// Map types can be accessed and modified like a normal Lua table a meta table.
|
||||
// Its length can also be queried using the # operator.
|
||||
//
|
||||
// Rather than using pairs to create an map iterator, calling the value (e.g.
|
||||
// map_variable()) will return an iterator for the map.
|
||||
//
|
||||
// Example:
|
||||
// places := map[string]string{
|
||||
// "NA": "North America",
|
||||
// "EU": "European Union",
|
||||
// }
|
||||
// L.SetGlobal("places", New(L, places))
|
||||
// ---
|
||||
// print(#places) -- prints "2"
|
||||
// print(places.NA) -- prints "North America"
|
||||
// print(places["EU"]) -- prints "European Union"
|
||||
// for k, v in places() do
|
||||
// print(k .. ": " .. v)
|
||||
// end
|
||||
//
|
||||
// Slice types
|
||||
//
|
||||
// Like map types, slices be accessed, be modified, and have their length
|
||||
// queried. Additionally, the following methods are defined for slices:
|
||||
// append(items...): Appends the items to the slice. Returns a slice with
|
||||
// the items appended.
|
||||
// capacity(): Returns the slice capacity.
|
||||
//
|
||||
// For consistency with other Lua code, slices use one-based indexing.
|
||||
//
|
||||
// Example:
|
||||
// letters := []string{"a", "e", "i"}
|
||||
// L.SetGlobal("letters", New(L, letters))
|
||||
// ---
|
||||
// letters = letters:append("o", "u")
|
||||
//
|
||||
// Struct types
|
||||
//
|
||||
// Struct types can have their fields accessed and modified and their methods
|
||||
// called. First letters of field/method names are automatically converted to
|
||||
// uppercase.
|
||||
//
|
||||
// Example:
|
||||
// type Person {
|
||||
// Name string
|
||||
// }
|
||||
// func (p Person) SayHello() {
|
||||
// fmt.Printf("Hello, %s\n", p.Name)
|
||||
// }
|
||||
//
|
||||
// tim := Person{"Tim"}
|
||||
// L.SetGlobal("tim", New(L, tim))
|
||||
// ---
|
||||
// tim:SayHello() -- same as tim:sayHello()
|
||||
//
|
||||
// Pointer types
|
||||
//
|
||||
// Pointers to structs operate the same way structs do. Pointers can also be
|
||||
// dereferenced using the unary minus (-) operator.
|
||||
//
|
||||
// Example:
|
||||
// str := "hello"
|
||||
// L.SetGlobal("strptr", New(L, &str))
|
||||
// ---
|
||||
// print(-strptr) -- prints "hello"
|
||||
//
|
||||
// The pointed to value can changed using the pow (^) operator.
|
||||
//
|
||||
// Example:
|
||||
// str := "hello"
|
||||
// L.SetGlobal("strptr", New(L, &str))
|
||||
// ---
|
||||
// print(str^"world") -- prints "world", and str's value is now "world"
|
||||
//
|
||||
// Type types
|
||||
//
|
||||
// Type constructors can be created using NewType. When called, it returns a
|
||||
// new variable which is of the same type that was passed to NewType. Its
|
||||
// behavior is dependent on the kind of value passed, as described below:
|
||||
//
|
||||
// Kind Constructor arguments Return value
|
||||
// -----------------------------------------------------
|
||||
// Channel Buffer size (opt) Channel
|
||||
// Map None Map
|
||||
// Slice Length (opt), Capacity (opt) Slice
|
||||
// Default None Pointer to the newly allocated value
|
||||
//
|
||||
// Example:
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// }
|
||||
// L.SetGlobal("Person", NewType(L, Person{}))
|
||||
// ---
|
||||
// p = Person()
|
||||
// p.Name = "John"
|
||||
// print("Hello, " .. p.Name) // prints "Hello, John"
|
||||
package luar
|
|
@ -0,0 +1,781 @@
|
|||
package luar_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/layeh/gopher-luar"
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
Friend *Person
|
||||
}
|
||||
|
||||
func (p Person) Hello() string {
|
||||
return "Hello, " + p.Name
|
||||
}
|
||||
|
||||
func (p Person) String() string {
|
||||
return p.Name + " (" + strconv.Itoa(p.Age) + ")"
|
||||
}
|
||||
|
||||
func (p *Person) AddNumbers(L *luar.LState) int {
|
||||
sum := 0
|
||||
for i := L.GetTop(); i >= 1; i-- {
|
||||
sum += L.CheckInt(i)
|
||||
}
|
||||
L.Push(lua.LString("Tim counts: " + strconv.Itoa(sum)))
|
||||
return 1
|
||||
}
|
||||
|
||||
type Proxy struct {
|
||||
XYZ string
|
||||
}
|
||||
|
||||
func (p *Proxy) LuarCall(args ...lua.LValue) {
|
||||
fmt.Printf("I was called with %d arguments!\n", len(args))
|
||||
}
|
||||
|
||||
func (p *Proxy) LuarNewIndex(key string, value lua.LValue) {
|
||||
str := value.String()
|
||||
p.XYZ = str + str
|
||||
}
|
||||
|
||||
func Example__1() {
|
||||
const code = `
|
||||
print(user1.Name)
|
||||
print(user1.Age)
|
||||
print(user1:Hello())
|
||||
|
||||
print(user2.Name)
|
||||
print(user2.Age)
|
||||
hello = user2.Hello
|
||||
print(hello(user2))
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
tim := &Person{
|
||||
Name: "Tim",
|
||||
Age: 30,
|
||||
}
|
||||
|
||||
john := Person{
|
||||
Name: "John",
|
||||
Age: 40,
|
||||
}
|
||||
|
||||
L.SetGlobal("user1", luar.New(L, tim))
|
||||
L.SetGlobal("user2", luar.New(L, john))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Output:
|
||||
// Tim
|
||||
// 30
|
||||
// Hello, Tim
|
||||
// John
|
||||
// 40
|
||||
// Hello, John
|
||||
}
|
||||
|
||||
func Example__2() {
|
||||
const code = `
|
||||
for i = 1, #things do
|
||||
print(things[i])
|
||||
end
|
||||
things[1] = "cookie"
|
||||
|
||||
print()
|
||||
|
||||
print(thangs.ABC)
|
||||
print(thangs.DEF)
|
||||
print(thangs.GHI)
|
||||
thangs.GHI = 789
|
||||
thangs.ABC = nil
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
things := []string{
|
||||
"cake",
|
||||
"wallet",
|
||||
"calendar",
|
||||
"phone",
|
||||
"speaker",
|
||||
}
|
||||
|
||||
thangs := map[string]int{
|
||||
"ABC": 123,
|
||||
"DEF": 456,
|
||||
}
|
||||
|
||||
L.SetGlobal("things", luar.New(L, things))
|
||||
L.SetGlobal("thangs", luar.New(L, thangs))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println(things[0])
|
||||
fmt.Println(thangs["GHI"])
|
||||
_, ok := thangs["ABC"]
|
||||
fmt.Println(ok)
|
||||
// Output:
|
||||
// cake
|
||||
// wallet
|
||||
// calendar
|
||||
// phone
|
||||
// speaker
|
||||
//
|
||||
// 123
|
||||
// 456
|
||||
// nil
|
||||
//
|
||||
// cookie
|
||||
// 789
|
||||
// false
|
||||
}
|
||||
|
||||
func Example__3() {
|
||||
const code = `
|
||||
user2 = Person()
|
||||
user2.Name = "John"
|
||||
user2.Friend = user1
|
||||
print(user2.Name)
|
||||
print(user2.Friend.Name)
|
||||
|
||||
everyone = People()
|
||||
everyone["tim"] = user1
|
||||
everyone["john"] = user2
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
tim := &Person{
|
||||
Name: "Tim",
|
||||
}
|
||||
|
||||
L.SetGlobal("user1", luar.New(L, tim))
|
||||
L.SetGlobal("Person", luar.NewType(L, Person{}))
|
||||
L.SetGlobal("People", luar.NewType(L, map[string]*Person{}))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
everyone := L.GetGlobal("everyone").(*lua.LUserData).Value.(map[string]*Person)
|
||||
fmt.Println(len(everyone))
|
||||
// Output:
|
||||
// John
|
||||
// Tim
|
||||
// 2
|
||||
}
|
||||
|
||||
func Example__4() {
|
||||
const code = `
|
||||
print(getHello(person))
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
tim := &Person{
|
||||
Name: "Tim",
|
||||
}
|
||||
|
||||
fn := func(p *Person) string {
|
||||
return "Hello, " + p.Name
|
||||
}
|
||||
|
||||
L.SetGlobal("person", luar.New(L, tim))
|
||||
L.SetGlobal("getHello", luar.New(L, fn))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Output:
|
||||
// Hello, Tim
|
||||
}
|
||||
|
||||
func Example__5() {
|
||||
const code = `
|
||||
print(ch:receive())
|
||||
ch:send("John")
|
||||
print(ch:receive())
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
ch := make(chan string)
|
||||
go func() {
|
||||
ch <- "Tim"
|
||||
name, ok := <-ch
|
||||
fmt.Printf("%s\t%v\n", name, ok)
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
L.SetGlobal("ch", luar.New(L, ch))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Output:
|
||||
// Tim true
|
||||
// John true
|
||||
// nil false
|
||||
}
|
||||
|
||||
func Example__6() {
|
||||
const code = `
|
||||
local sorted = {}
|
||||
for k, v in countries() do
|
||||
table.insert(sorted, v)
|
||||
end
|
||||
table.sort(sorted)
|
||||
for i = 1, #sorted do
|
||||
print(sorted[i])
|
||||
end
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
countries := map[string]string{
|
||||
"JP": "Japan",
|
||||
"CA": "Canada",
|
||||
"FR": "France",
|
||||
}
|
||||
|
||||
L.SetGlobal("countries", luar.New(L, countries))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Output:
|
||||
// Canada
|
||||
// France
|
||||
// Japan
|
||||
}
|
||||
|
||||
func Example__7() {
|
||||
const code = `
|
||||
fn("a", 1, 2, 3)
|
||||
fn("b")
|
||||
fn("c", 4)
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
fn := func(str string, extra ...int) {
|
||||
fmt.Printf("%s\n", str)
|
||||
for _, x := range extra {
|
||||
fmt.Printf("%d\n", x)
|
||||
}
|
||||
}
|
||||
|
||||
L.SetGlobal("fn", luar.New(L, fn))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Output:
|
||||
// a
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
// b
|
||||
// c
|
||||
// 4
|
||||
}
|
||||
|
||||
func Example__8() {
|
||||
const code = `
|
||||
for _, x in ipairs(fn(1, 2, 3)) do
|
||||
print(x)
|
||||
end
|
||||
for _, x in ipairs(fn()) do
|
||||
print(x)
|
||||
end
|
||||
for _, x in ipairs(fn(4)) do
|
||||
print(x)
|
||||
end
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
fn := func(x ...float64) *lua.LTable {
|
||||
tbl := L.NewTable()
|
||||
for i := len(x) - 1; i >= 0; i-- {
|
||||
tbl.Insert(len(x)-i, lua.LNumber(x[i]))
|
||||
}
|
||||
return tbl
|
||||
}
|
||||
|
||||
L.SetGlobal("fn", luar.New(L, fn))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Output:
|
||||
// 3
|
||||
// 2
|
||||
// 1
|
||||
// 4
|
||||
}
|
||||
|
||||
func Example__9() {
|
||||
const code = `
|
||||
print(#items)
|
||||
print(items:capacity())
|
||||
items = items:append("hello", "world")
|
||||
print(#items)
|
||||
print(items:capacity())
|
||||
print(items[1])
|
||||
print(items[2])
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
items := make([]string, 0, 10)
|
||||
|
||||
L.SetGlobal("items", luar.New(L, items))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Output:
|
||||
// 0
|
||||
// 10
|
||||
// 2
|
||||
// 10
|
||||
// hello
|
||||
// world
|
||||
}
|
||||
|
||||
func Example__10() {
|
||||
const code = `
|
||||
ints = newInts(1)
|
||||
print(#ints, ints:capacity())
|
||||
|
||||
ints = newInts(0, 10)
|
||||
print(#ints, ints:capacity())
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
type ints []int
|
||||
|
||||
L.SetGlobal("newInts", luar.NewType(L, ints{}))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Output:
|
||||
// 1 1
|
||||
// 0 10
|
||||
}
|
||||
|
||||
func Example__11() {
|
||||
const code = `
|
||||
print(p1 == p1)
|
||||
print(p1 == p1_alias)
|
||||
print(p1 == p2)
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
p1 := Person{
|
||||
Name: "Tim",
|
||||
}
|
||||
p2 := Person{
|
||||
Name: "John",
|
||||
}
|
||||
|
||||
L.SetGlobal("p1", luar.New(L, &p1))
|
||||
L.SetGlobal("p1_alias", luar.New(L, &p1))
|
||||
L.SetGlobal("p2", luar.New(L, &p2))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Output:
|
||||
// true
|
||||
// true
|
||||
// false
|
||||
}
|
||||
|
||||
func Example__12() {
|
||||
const code = `
|
||||
print(p1)
|
||||
print(p2)
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
p1 := Person{
|
||||
Name: "Tim",
|
||||
Age: 99,
|
||||
}
|
||||
p2 := Person{
|
||||
Name: "John",
|
||||
Age: 2,
|
||||
}
|
||||
|
||||
L.SetGlobal("p1", luar.New(L, &p1))
|
||||
L.SetGlobal("p2", luar.New(L, &p2))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Output:
|
||||
// Tim (99)
|
||||
// John (2)
|
||||
}
|
||||
|
||||
func Example__13() {
|
||||
const code = `
|
||||
print(p:AddNumbers(1, 2, 3, 4, 5))
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
p := Person{
|
||||
Name: "Tim",
|
||||
}
|
||||
|
||||
L.SetGlobal("p", luar.New(L, &p))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Output:
|
||||
// Tim counts: 15
|
||||
}
|
||||
|
||||
func Example__14() {
|
||||
const code = `
|
||||
print(p:hello())
|
||||
print(p.age)
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
p := Person{
|
||||
Name: "Tim",
|
||||
Age: 66,
|
||||
}
|
||||
|
||||
L.SetGlobal("p", luar.New(L, &p))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Output:
|
||||
// Hello, Tim
|
||||
// 66
|
||||
}
|
||||
|
||||
func Example__15() {
|
||||
const code = `
|
||||
print(p.XYZ)
|
||||
p("Hello", "World")
|
||||
p.nothing = "nice"
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
p := Proxy{
|
||||
XYZ: "1000+",
|
||||
}
|
||||
|
||||
L.SetGlobal("p", luar.New(L, &p))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(p.XYZ)
|
||||
// Output:
|
||||
// 1000+
|
||||
// I was called with 2 arguments!
|
||||
// nicenice
|
||||
}
|
||||
|
||||
func Example__16() {
|
||||
const code = `
|
||||
print(fn("tim", 5))
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
fn := func(name string, count int) []lua.LValue {
|
||||
s := make([]lua.LValue, count)
|
||||
for i := 0; i < count; i++ {
|
||||
s[i] = lua.LString(name)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
L.SetGlobal("fn", luar.New(L, fn))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Output:
|
||||
// tim tim tim tim tim
|
||||
}
|
||||
|
||||
func Example__17() {
|
||||
const code = `
|
||||
print(-ptr)
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
str := "hello"
|
||||
|
||||
L.SetGlobal("ptr", luar.New(L, &str))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Output:
|
||||
// hello
|
||||
}
|
||||
|
||||
func Example__18() {
|
||||
const code = `
|
||||
print(ptr1 == nil)
|
||||
print(ptr2 == nil)
|
||||
print(ptr1 == ptr2)
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
var ptr1 *string
|
||||
str := "hello"
|
||||
|
||||
L.SetGlobal("ptr1", luar.New(L, ptr1))
|
||||
L.SetGlobal("ptr2", luar.New(L, &str))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Output:
|
||||
// true
|
||||
// false
|
||||
// false
|
||||
}
|
||||
|
||||
func Example__19() {
|
||||
const code = `
|
||||
print(-str)
|
||||
print(str ^ "world")
|
||||
print(-str)
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
str := "hello"
|
||||
|
||||
L.SetGlobal("str", luar.New(L, &str))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Output:
|
||||
// hello
|
||||
// world
|
||||
// world
|
||||
}
|
||||
|
||||
type Example__20_A struct {
|
||||
*Example__20_B
|
||||
}
|
||||
|
||||
type Example__20_B struct {
|
||||
Value *string
|
||||
}
|
||||
|
||||
func Example__20() {
|
||||
const code = `
|
||||
print(a.Value == nil)
|
||||
a.Value = str_ptr()
|
||||
_ = a.Value ^ "hello"
|
||||
print(a.Value == nil)
|
||||
print(-a.Value)
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
a := Example__20_A{
|
||||
Example__20_B: &Example__20_B{},
|
||||
}
|
||||
|
||||
L.SetGlobal("a", luar.New(L, a))
|
||||
L.SetGlobal("str_ptr", luar.NewType(L, ""))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Output:
|
||||
// true
|
||||
// false
|
||||
// hello
|
||||
}
|
||||
|
||||
func Example__21() {
|
||||
const code = `
|
||||
print(fn == nil)
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
var fn func()
|
||||
|
||||
L.SetGlobal("fn", luar.New(L, fn))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Output:
|
||||
// true
|
||||
}
|
||||
|
||||
func Example__22() {
|
||||
const code = `
|
||||
fn(arr)
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
arr := [3]int{1, 2, 3}
|
||||
fn := func(val [3]int) {
|
||||
fmt.Printf("%d %d %d\n", val[0], val[1], val[2])
|
||||
}
|
||||
|
||||
L.SetGlobal("fn", luar.New(L, fn))
|
||||
L.SetGlobal("arr", luar.New(L, arr))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Output:
|
||||
// 1 2 3
|
||||
}
|
||||
|
||||
func Example__23() {
|
||||
const code = `
|
||||
b = a
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
a := complex(float64(1), float64(2))
|
||||
|
||||
L.SetGlobal("a", luar.New(L, a))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
b := L.GetGlobal("b").(*lua.LUserData).Value.(complex128)
|
||||
fmt.Println(a == b)
|
||||
// Output:
|
||||
// true
|
||||
}
|
||||
|
||||
func ExampleMeta() {
|
||||
const code = `
|
||||
proxy(234, nil, "asd", {})
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
// Proxy has the following method defined:
|
||||
// func (p *Proxy) LuarCall(args ...lua.LValue) {
|
||||
// fmt.Printf("I was called with %d arguments!\n", len(args))
|
||||
// }
|
||||
//
|
||||
proxy := &Proxy{}
|
||||
|
||||
L.SetGlobal("proxy", luar.New(L, proxy))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Output:
|
||||
// I was called with 4 arguments!
|
||||
}
|
||||
|
||||
func ExampleLState() {
|
||||
const code = `
|
||||
print(sum(1, 2, 3, 4, 5))
|
||||
`
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
sum := func(L *luar.LState) int {
|
||||
total := 0
|
||||
for i := 1; i <= L.GetTop(); i++ {
|
||||
total += L.CheckInt(i)
|
||||
}
|
||||
L.Push(lua.LNumber(total))
|
||||
return 1
|
||||
}
|
||||
|
||||
L.SetGlobal("sum", luar.New(L, sum))
|
||||
|
||||
if err := L.DoString(code); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Output:
|
||||
// 15
|
||||
}
|
||||
|
||||
func ExampleNewType() {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
type Song struct {
|
||||
Title string
|
||||
Artist string
|
||||
}
|
||||
|
||||
L.SetGlobal("Song", luar.NewType(L, Song{}))
|
||||
L.DoString(`
|
||||
s = Song()
|
||||
s.Title = "Montana"
|
||||
s.Artist = "Tycho"
|
||||
print(s.Artist .. " - " .. s.Title)
|
||||
`)
|
||||
// Output:
|
||||
// Tycho - Montana
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package luar
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// LState is an wrapper for gopher-lua's LState. It should be used when you
|
||||
// wish to have a function/method with the standard "func(*lua.LState) int"
|
||||
// signature.
|
||||
type LState struct {
|
||||
*lua.LState
|
||||
}
|
||||
|
||||
var (
|
||||
refTypeLStatePtr reflect.Type
|
||||
refTypeLuaLValueSlice reflect.Type
|
||||
refTypeLuaLValue reflect.Type
|
||||
refTypeInt reflect.Type
|
||||
)
|
||||
|
||||
func init() {
|
||||
refTypeLStatePtr = reflect.TypeOf(&LState{})
|
||||
refTypeLuaLValueSlice = reflect.TypeOf([]lua.LValue{})
|
||||
refTypeLuaLValue = reflect.TypeOf((*lua.LValue)(nil)).Elem()
|
||||
refTypeInt = reflect.TypeOf(int(0))
|
||||
}
|
||||
|
||||
func funcIsBypass(t reflect.Type) bool {
|
||||
if t.NumIn() == 1 && t.NumOut() == 1 && t.In(0) == refTypeLStatePtr && t.Out(0) == refTypeInt {
|
||||
return true
|
||||
}
|
||||
if t.NumIn() == 2 && t.NumOut() == 1 && t.In(1) == refTypeLStatePtr && t.Out(0) == refTypeInt {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func funcEvaluate(L *lua.LState, fn reflect.Value) int {
|
||||
fnType := fn.Type()
|
||||
if funcIsBypass(fnType) {
|
||||
luarState := LState{L}
|
||||
args := make([]reflect.Value, 0, 2)
|
||||
if fnType.NumIn() == 2 {
|
||||
receiverHint := fnType.In(0)
|
||||
receiver := lValueToReflect(L.Get(1), receiverHint)
|
||||
if receiver.Type() != receiverHint {
|
||||
L.RaiseError("incorrect receiver type")
|
||||
}
|
||||
args = append(args, receiver)
|
||||
L.Remove(1)
|
||||
}
|
||||
args = append(args, reflect.ValueOf(&luarState))
|
||||
return fn.Call(args)[0].Interface().(int)
|
||||
}
|
||||
|
||||
top := L.GetTop()
|
||||
expected := fnType.NumIn()
|
||||
variadic := fnType.IsVariadic()
|
||||
if !variadic && top != expected {
|
||||
L.RaiseError("invalid number of function arguments (%d expected, got %d)", expected, top)
|
||||
}
|
||||
if variadic && top < expected-1 {
|
||||
L.RaiseError("invalid number of function arguments (%d or more expected, got %d)", expected-1, top)
|
||||
}
|
||||
args := make([]reflect.Value, top)
|
||||
for i := 0; i < L.GetTop(); i++ {
|
||||
var hint reflect.Type
|
||||
if variadic && i >= expected-1 {
|
||||
hint = fnType.In(expected - 1).Elem()
|
||||
} else {
|
||||
hint = fnType.In(i)
|
||||
}
|
||||
args[i] = lValueToReflect(L.Get(i+1), hint)
|
||||
}
|
||||
ret := fn.Call(args)
|
||||
if len(ret) == 1 && ret[0].Type() == refTypeLuaLValueSlice {
|
||||
values := ret[0].Interface().([]lua.LValue)
|
||||
for _, value := range values {
|
||||
L.Push(value)
|
||||
}
|
||||
return len(values)
|
||||
}
|
||||
for _, val := range ret {
|
||||
L.Push(New(L, val.Interface()))
|
||||
}
|
||||
return len(ret)
|
||||
}
|
||||
|
||||
func funcWrapper(L *lua.LState, fn reflect.Value) *lua.LFunction {
|
||||
wrapper := func(L *lua.LState) int {
|
||||
return funcEvaluate(L, fn)
|
||||
}
|
||||
return L.NewFunction(wrapper)
|
||||
}
|
|
@ -0,0 +1,226 @@
|
|||
package luar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
var wrapperMetatable map[string]map[string]lua.LGFunction
|
||||
|
||||
func init() {
|
||||
wrapperMetatable = map[string]map[string]lua.LGFunction{
|
||||
"chan": {
|
||||
"__index": chanIndex,
|
||||
"__len": chanLen,
|
||||
"__tostring": allTostring,
|
||||
"__eq": chanEq,
|
||||
},
|
||||
"map": {
|
||||
"__index": mapIndex,
|
||||
"__newindex": mapNewIndex,
|
||||
"__len": mapLen,
|
||||
"__call": mapCall,
|
||||
"__tostring": allTostring,
|
||||
"__eq": mapEq,
|
||||
},
|
||||
"ptr": {
|
||||
"__index": ptrIndex,
|
||||
"__newindex": ptrNewIndex,
|
||||
"__pow": ptrPow,
|
||||
"__call": ptrCall,
|
||||
"__tostring": allTostring,
|
||||
"__unm": ptrUnm,
|
||||
"__eq": ptrEq,
|
||||
},
|
||||
"slice": {
|
||||
"__index": sliceIndex,
|
||||
"__newindex": sliceNewIndex,
|
||||
"__len": sliceLen,
|
||||
"__tostring": allTostring,
|
||||
"__eq": sliceEq,
|
||||
},
|
||||
"struct": {
|
||||
"__index": structIndex,
|
||||
"__newindex": structNewIndex,
|
||||
"__call": structCall,
|
||||
"__tostring": allTostring,
|
||||
},
|
||||
"type": {
|
||||
"__call": typeCall,
|
||||
"__tostring": allTostring,
|
||||
"__eq": typeEq,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ensureMetatable(L *lua.LState) *lua.LTable {
|
||||
const metatableKey = lua.LString("github.com/layeh/gopher-luar")
|
||||
v := L.G.Registry.RawGetH(metatableKey)
|
||||
if v != lua.LNil {
|
||||
return v.(*lua.LTable)
|
||||
}
|
||||
newTable := L.NewTable()
|
||||
|
||||
for typeName, meta := range wrapperMetatable {
|
||||
typeTable := L.NewTable()
|
||||
typeTable.RawSetH(lua.LString("__metatable"), lua.LTrue)
|
||||
for methodName, methodFunc := range meta {
|
||||
typeTable.RawSetH(lua.LString(methodName), L.NewFunction(methodFunc))
|
||||
}
|
||||
newTable.RawSetH(lua.LString(typeName), typeTable)
|
||||
}
|
||||
|
||||
L.G.Registry.RawSetH(metatableKey, newTable)
|
||||
return newTable
|
||||
}
|
||||
|
||||
func allTostring(L *lua.LState) int {
|
||||
ud := L.CheckUserData(1)
|
||||
value := ud.Value
|
||||
if stringer, ok := value.(fmt.Stringer); ok {
|
||||
L.Push(lua.LString(stringer.String()))
|
||||
} else {
|
||||
L.Push(lua.LString(fmt.Sprintf("userdata (luar): %p", ud)))
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// New creates and returns a new lua.LValue for the given value.
|
||||
//
|
||||
// The following types are supported:
|
||||
// Kind gopher-lua Type
|
||||
// -------------------------------
|
||||
// nil LNil
|
||||
// Bool LBool
|
||||
// Int LNumber
|
||||
// Int8 LNumber
|
||||
// Int16 LNumber
|
||||
// Int32 LNumber
|
||||
// Int64 LNumber
|
||||
// Uint LNumber
|
||||
// Uint8 LNumber
|
||||
// Uint32 LNumber
|
||||
// Uint64 LNumber
|
||||
// Float32 LNumber
|
||||
// Float64 LNumber
|
||||
// Complex64 *LUserData
|
||||
// Complex128 *LUserData
|
||||
// Array *LUserData
|
||||
// Chan *LUserData
|
||||
// Interface *LUserData
|
||||
// Func *lua.LFunction
|
||||
// Map *LUserData
|
||||
// Ptr *LUserData
|
||||
// Slice *LUserData
|
||||
// String LString
|
||||
// Struct *LUserData
|
||||
// UnsafePointer *LUserData
|
||||
func New(L *lua.LState, value interface{}) lua.LValue {
|
||||
if value == nil {
|
||||
return lua.LNil
|
||||
}
|
||||
if lval, ok := value.(lua.LValue); ok {
|
||||
return lval
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(value)
|
||||
switch val.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
|
||||
if val.IsNil() {
|
||||
return lua.LNil
|
||||
}
|
||||
}
|
||||
|
||||
table := ensureMetatable(L)
|
||||
|
||||
switch val.Kind() {
|
||||
case reflect.Bool:
|
||||
return lua.LBool(val.Bool())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return lua.LNumber(float64(val.Int()))
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return lua.LNumber(float64(val.Uint()))
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return lua.LNumber(val.Float())
|
||||
case reflect.Chan:
|
||||
ud := L.NewUserData()
|
||||
ud.Value = val.Interface()
|
||||
ud.Metatable = table.RawGetH(lua.LString("chan"))
|
||||
return ud
|
||||
case reflect.Func:
|
||||
return funcWrapper(L, val)
|
||||
case reflect.Interface:
|
||||
ud := L.NewUserData()
|
||||
ud.Value = val.Interface()
|
||||
return ud
|
||||
case reflect.Map:
|
||||
ud := L.NewUserData()
|
||||
ud.Value = val.Interface()
|
||||
ud.Metatable = table.RawGetH(lua.LString("map"))
|
||||
return ud
|
||||
case reflect.Ptr:
|
||||
ud := L.NewUserData()
|
||||
ud.Value = val.Interface()
|
||||
ud.Metatable = table.RawGetH(lua.LString("ptr"))
|
||||
return ud
|
||||
case reflect.Slice:
|
||||
ud := L.NewUserData()
|
||||
ud.Value = val.Interface()
|
||||
ud.Metatable = table.RawGetH(lua.LString("slice"))
|
||||
return ud
|
||||
case reflect.String:
|
||||
return lua.LString(val.String())
|
||||
case reflect.Struct:
|
||||
ud := L.NewUserData()
|
||||
ud.Value = val.Interface()
|
||||
ud.Metatable = table.RawGetH(lua.LString("struct"))
|
||||
return ud
|
||||
default:
|
||||
ud := L.NewUserData()
|
||||
ud.Value = val.Interface()
|
||||
return ud
|
||||
}
|
||||
}
|
||||
|
||||
// NewType returns a new type creator for the given value's type.
|
||||
//
|
||||
// When the lua.LValue is called, a new value will be created that is the
|
||||
// same type as value's type.
|
||||
func NewType(L *lua.LState, value interface{}) lua.LValue {
|
||||
table := ensureMetatable(L)
|
||||
|
||||
val := reflect.TypeOf(value)
|
||||
ud := L.NewUserData()
|
||||
ud.Value = val
|
||||
ud.Metatable = table.RawGetH(lua.LString("type"))
|
||||
return ud
|
||||
}
|
||||
|
||||
func lValueToReflect(v lua.LValue, hint reflect.Type) reflect.Value {
|
||||
if hint == refTypeLuaLValue {
|
||||
return reflect.ValueOf(v)
|
||||
}
|
||||
switch converted := v.(type) {
|
||||
case lua.LBool:
|
||||
return reflect.ValueOf(bool(converted))
|
||||
case lua.LChannel:
|
||||
return reflect.ValueOf(converted)
|
||||
case lua.LNumber:
|
||||
return reflect.ValueOf(converted).Convert(hint)
|
||||
case *lua.LFunction:
|
||||
return reflect.ValueOf(converted)
|
||||
case *lua.LNilType:
|
||||
return reflect.Zero(hint)
|
||||
case *lua.LState:
|
||||
return reflect.ValueOf(converted)
|
||||
case lua.LString:
|
||||
return reflect.ValueOf(string(converted))
|
||||
case *lua.LTable:
|
||||
return reflect.ValueOf(converted)
|
||||
case *lua.LUserData:
|
||||
return reflect.ValueOf(converted.Value)
|
||||
}
|
||||
panic("fatal lValueToReflect error")
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package luar
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func checkMap(L *lua.LState, idx int) reflect.Value {
|
||||
ud := L.CheckUserData(idx)
|
||||
ref := reflect.ValueOf(ud.Value)
|
||||
if ref.Kind() != reflect.Map {
|
||||
L.ArgError(idx, "expecting map")
|
||||
}
|
||||
return ref
|
||||
}
|
||||
|
||||
func mapIndex(L *lua.LState) int {
|
||||
ref := checkMap(L, 1)
|
||||
key := L.CheckAny(2)
|
||||
|
||||
convertedKey := lValueToReflect(key, ref.Type().Key())
|
||||
item := ref.MapIndex(convertedKey)
|
||||
if !item.IsValid() {
|
||||
return 0
|
||||
}
|
||||
L.Push(New(L, item.Interface()))
|
||||
return 1
|
||||
}
|
||||
|
||||
func mapNewIndex(L *lua.LState) int {
|
||||
ref := checkMap(L, 1)
|
||||
key := L.CheckAny(2)
|
||||
value := L.CheckAny(3)
|
||||
|
||||
convertedKey := lValueToReflect(key, ref.Type().Key())
|
||||
if convertedKey.Type() != ref.Type().Key() {
|
||||
L.ArgError(2, "invalid map key type")
|
||||
}
|
||||
var convertedValue reflect.Value
|
||||
if value != lua.LNil {
|
||||
convertedValue = lValueToReflect(value, ref.Type().Elem())
|
||||
if convertedValue.Type() != ref.Type().Elem() {
|
||||
L.ArgError(3, "invalid map value type")
|
||||
}
|
||||
}
|
||||
ref.SetMapIndex(convertedKey, convertedValue)
|
||||
return 0
|
||||
}
|
||||
|
||||
func mapLen(L *lua.LState) int {
|
||||
ref := checkMap(L, 1)
|
||||
L.Push(lua.LNumber(ref.Len()))
|
||||
return 1
|
||||
}
|
||||
|
||||
func mapCall(L *lua.LState) int {
|
||||
ref := checkMap(L, 1)
|
||||
keys := ref.MapKeys()
|
||||
i := 0
|
||||
fn := func(L *lua.LState) int {
|
||||
if i >= len(keys) {
|
||||
return 0
|
||||
}
|
||||
L.Push(New(L, keys[i].Interface()))
|
||||
L.Push(New(L, ref.MapIndex(keys[i]).Interface()))
|
||||
i++
|
||||
return 2
|
||||
}
|
||||
L.Push(L.NewFunction(fn))
|
||||
return 1
|
||||
}
|
||||
|
||||
func mapEq(L *lua.LState) int {
|
||||
map1 := checkMap(L, 1)
|
||||
map2 := checkMap(L, 2)
|
||||
L.Push(lua.LBool(map1 == map2))
|
||||
return 1
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package luar
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// Meta can be implemented by a struct or struct pointer. Each method defines
|
||||
// a fallback action for the corresponding Lua metamethod.
|
||||
//
|
||||
// The signature of the methods does not matter; they will be converted using
|
||||
// the standard function conversion rules. Also, a type is allowed to implement
|
||||
// only a subset of the interface.
|
||||
type Meta interface {
|
||||
LuarCall(arguments ...interface{}) interface{}
|
||||
LuarIndex(key interface{}) interface{}
|
||||
LuarNewIndex(key, value interface{})
|
||||
}
|
||||
|
||||
const (
|
||||
luarCallFunc = "LuarCall"
|
||||
luarIndexFunc = "LuarIndex"
|
||||
luarNewIndexFunc = "LuarNewIndex"
|
||||
)
|
||||
|
||||
func metaFunction(L *lua.LState, name string, ref reflect.Value) int {
|
||||
refType := ref.Type()
|
||||
method, ok := refType.MethodByName(name)
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
return funcEvaluate(L, method.Func)
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
package luar
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func checkPtr(L *lua.LState, idx int) reflect.Value {
|
||||
ud := L.CheckUserData(idx)
|
||||
ref := reflect.ValueOf(ud.Value)
|
||||
if ref.Kind() != reflect.Ptr {
|
||||
L.ArgError(idx, "expecting ptr")
|
||||
}
|
||||
return ref
|
||||
}
|
||||
|
||||
func ptrIndex(L *lua.LState) int {
|
||||
ref := checkPtr(L, 1)
|
||||
deref := ref.Elem()
|
||||
if deref.Kind() != reflect.Struct {
|
||||
L.RaiseError("cannot index non-struct pointer")
|
||||
}
|
||||
refType := ref.Type()
|
||||
|
||||
// Check for method
|
||||
key := L.OptString(2, "")
|
||||
exKey := getExportedName(key)
|
||||
if key != "" {
|
||||
if method, ok := refType.MethodByName(exKey); ok {
|
||||
L.Push(New(L, method.Func.Interface()))
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// Check for field
|
||||
if field := deref.FieldByName(exKey); field.IsValid() {
|
||||
if !field.CanInterface() {
|
||||
L.RaiseError("cannot interface field " + exKey)
|
||||
}
|
||||
if val := New(L, field.Interface()); val != nil {
|
||||
L.Push(val)
|
||||
return 1
|
||||
}
|
||||
L.RaiseError("could not convert field " + exKey)
|
||||
}
|
||||
|
||||
if ret := metaFunction(L, luarIndexFunc, ref); ret >= 0 {
|
||||
return ret
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func ptrNewIndex(L *lua.LState) int {
|
||||
ref := checkPtr(L, 1)
|
||||
deref := ref.Elem()
|
||||
|
||||
if deref.Kind() != reflect.Struct {
|
||||
L.RaiseError("cannot new index non-struct pointer")
|
||||
}
|
||||
|
||||
key := L.OptString(2, "")
|
||||
value := L.CheckAny(3)
|
||||
|
||||
if key == "" {
|
||||
if ret := metaFunction(L, luarNewIndexFunc, ref); ret >= 0 {
|
||||
return ret
|
||||
}
|
||||
L.TypeError(2, lua.LTString)
|
||||
return 0
|
||||
}
|
||||
|
||||
exKey := getExportedName(key)
|
||||
|
||||
field := deref.FieldByName(exKey)
|
||||
if !field.IsValid() {
|
||||
if ret := metaFunction(L, luarNewIndexFunc, ref); ret >= 0 {
|
||||
return ret
|
||||
}
|
||||
L.ArgError(2, "unknown field "+exKey)
|
||||
}
|
||||
if !field.CanSet() {
|
||||
L.ArgError(2, "cannot set field "+exKey)
|
||||
}
|
||||
field.Set(lValueToReflect(value, field.Type()))
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func ptrPow(L *lua.LState) int {
|
||||
ref := checkPtr(L, 1)
|
||||
val := L.CheckAny(2)
|
||||
|
||||
if ref.IsNil() {
|
||||
L.RaiseError("cannot dereference nil pointer")
|
||||
}
|
||||
elem := ref.Elem()
|
||||
if !elem.CanSet() {
|
||||
L.RaiseError("unable to set pointer value")
|
||||
}
|
||||
value := lValueToReflect(val, elem.Type())
|
||||
elem.Set(value)
|
||||
return 1
|
||||
}
|
||||
|
||||
func ptrCall(L *lua.LState) int {
|
||||
ref := checkPtr(L, 1)
|
||||
if ret := metaFunction(L, luarCallFunc, ref); ret >= 0 {
|
||||
return ret
|
||||
}
|
||||
L.RaiseError("attempt to call a non-function object")
|
||||
return 0
|
||||
}
|
||||
|
||||
func ptrUnm(L *lua.LState) int {
|
||||
ref := checkPtr(L, 1)
|
||||
elem := ref.Elem()
|
||||
if !elem.CanInterface() {
|
||||
L.RaiseError("cannot interface pointer type " + elem.String())
|
||||
}
|
||||
L.Push(New(L, elem.Interface()))
|
||||
return 1
|
||||
}
|
||||
|
||||
func ptrEq(L *lua.LState) int {
|
||||
ptr1 := checkPtr(L, 1)
|
||||
ptr2 := checkPtr(L, 2)
|
||||
L.Push(lua.LBool(ptr1 == ptr2))
|
||||
return 1
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package luar
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func checkSlice(L *lua.LState, idx int) reflect.Value {
|
||||
ud := L.CheckUserData(idx)
|
||||
ref := reflect.ValueOf(ud.Value)
|
||||
if ref.Kind() != reflect.Slice {
|
||||
L.ArgError(idx, "expecting slice")
|
||||
}
|
||||
return ref
|
||||
}
|
||||
|
||||
func sliceIndex(L *lua.LState) int {
|
||||
ref := checkSlice(L, 1)
|
||||
key := L.CheckAny(2)
|
||||
|
||||
switch converted := key.(type) {
|
||||
case lua.LNumber:
|
||||
index := int(converted)
|
||||
if index < 1 || index > ref.Len() {
|
||||
L.ArgError(2, "index out of range")
|
||||
}
|
||||
L.Push(New(L, ref.Index(index-1).Interface()))
|
||||
case lua.LString:
|
||||
switch string(converted) {
|
||||
case "capacity":
|
||||
L.Push(L.NewFunction(sliceCapacity))
|
||||
case "append":
|
||||
L.Push(L.NewFunction(sliceAppend))
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
default:
|
||||
L.ArgError(2, "must be a number or string")
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func sliceNewIndex(L *lua.LState) int {
|
||||
ref := checkSlice(L, 1)
|
||||
index := L.CheckInt(2)
|
||||
value := L.CheckAny(3)
|
||||
|
||||
if index < 1 || index > ref.Len() {
|
||||
L.ArgError(2, "index out of range")
|
||||
}
|
||||
ref.Index(index - 1).Set(lValueToReflect(value, ref.Type().Elem()))
|
||||
return 0
|
||||
}
|
||||
|
||||
func sliceLen(L *lua.LState) int {
|
||||
ref := checkSlice(L, 1)
|
||||
L.Push(lua.LNumber(ref.Len()))
|
||||
return 1
|
||||
}
|
||||
|
||||
func sliceEq(L *lua.LState) int {
|
||||
slice1 := checkSlice(L, 1)
|
||||
slice2 := checkSlice(L, 2)
|
||||
L.Push(lua.LBool(slice1 == slice2))
|
||||
return 1
|
||||
}
|
||||
|
||||
// slice methods
|
||||
|
||||
func sliceCapacity(L *lua.LState) int {
|
||||
ref := checkSlice(L, 1)
|
||||
L.Push(lua.LNumber(ref.Cap()))
|
||||
return 1
|
||||
}
|
||||
|
||||
func sliceAppend(L *lua.LState) int {
|
||||
ref := checkSlice(L, 1)
|
||||
|
||||
hint := ref.Type().Elem()
|
||||
values := make([]reflect.Value, L.GetTop()-1)
|
||||
for i := 2; i <= L.GetTop(); i++ {
|
||||
value := lValueToReflect(L.Get(i), hint)
|
||||
if value.Type() != hint {
|
||||
L.ArgError(i, "invalid type")
|
||||
}
|
||||
values[i-2] = value
|
||||
}
|
||||
|
||||
newSlice := reflect.Append(ref, values...)
|
||||
L.Push(New(L, newSlice.Interface()))
|
||||
return 1
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package luar
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func checkStruct(L *lua.LState, idx int) reflect.Value {
|
||||
ud := L.CheckUserData(idx)
|
||||
ref := reflect.ValueOf(ud.Value)
|
||||
if ref.Kind() != reflect.Struct {
|
||||
L.ArgError(idx, "expecting struct")
|
||||
}
|
||||
return ref
|
||||
}
|
||||
|
||||
func structIndex(L *lua.LState) int {
|
||||
ref := checkStruct(L, 1)
|
||||
refType := ref.Type()
|
||||
|
||||
// Check for method
|
||||
key := L.OptString(2, "")
|
||||
exKey := getExportedName(key)
|
||||
if exKey != "" {
|
||||
if method, ok := refType.MethodByName(exKey); ok {
|
||||
L.Push(New(L, method.Func.Interface()))
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// Check for field
|
||||
if field := ref.FieldByName(exKey); field.IsValid() {
|
||||
if !field.CanInterface() {
|
||||
L.RaiseError("cannot interface field " + exKey)
|
||||
}
|
||||
L.Push(New(L, field.Interface()))
|
||||
return 1
|
||||
}
|
||||
|
||||
if ret := metaFunction(L, luarIndexFunc, ref); ret >= 0 {
|
||||
return ret
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func structNewIndex(L *lua.LState) int {
|
||||
ref := checkStruct(L, 1)
|
||||
key := L.OptString(2, "")
|
||||
value := L.CheckAny(3)
|
||||
|
||||
if key == "" {
|
||||
if ret := metaFunction(L, luarNewIndexFunc, ref); ret >= 0 {
|
||||
return ret
|
||||
}
|
||||
L.TypeError(2, lua.LTString)
|
||||
return 0
|
||||
}
|
||||
|
||||
exKey := getExportedName(key)
|
||||
|
||||
field := ref.FieldByName(exKey)
|
||||
if !field.IsValid() {
|
||||
if ret := metaFunction(L, luarNewIndexFunc, ref); ret >= 0 {
|
||||
return ret
|
||||
}
|
||||
L.ArgError(2, "unknown field "+exKey)
|
||||
}
|
||||
if !field.CanSet() {
|
||||
L.ArgError(2, "cannot set field "+exKey)
|
||||
}
|
||||
field.Set(lValueToReflect(value, field.Type()))
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func structCall(L *lua.LState) int {
|
||||
ref := checkStruct(L, 1)
|
||||
if ret := metaFunction(L, luarCallFunc, ref); ret >= 0 {
|
||||
return ret
|
||||
}
|
||||
L.RaiseError("attempt to call a non-function object")
|
||||
return 0
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package luar
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func checkType(L *lua.LState, idx int) reflect.Type {
|
||||
ud := L.CheckUserData(idx)
|
||||
ref, ok := ud.Value.(reflect.Type)
|
||||
if !ok {
|
||||
L.ArgError(idx, "expecting type")
|
||||
}
|
||||
return ref
|
||||
}
|
||||
|
||||
func typeCall(L *lua.LState) int {
|
||||
ref := checkType(L, 1)
|
||||
|
||||
var value reflect.Value
|
||||
switch ref.Kind() {
|
||||
case reflect.Chan:
|
||||
buffer := L.OptInt(2, 0)
|
||||
value = reflect.MakeChan(ref, buffer)
|
||||
case reflect.Map:
|
||||
value = reflect.MakeMap(ref)
|
||||
case reflect.Slice:
|
||||
length := L.OptInt(2, 0)
|
||||
capacity := L.OptInt(3, length)
|
||||
value = reflect.MakeSlice(ref, length, capacity)
|
||||
default:
|
||||
value = reflect.New(ref)
|
||||
}
|
||||
L.Push(New(L, value.Interface()))
|
||||
return 1
|
||||
}
|
||||
|
||||
func typeEq(L *lua.LState) int {
|
||||
type1 := checkType(L, 1)
|
||||
type2 := checkType(L, 2)
|
||||
L.Push(lua.LBool(type1 == type2))
|
||||
return 1
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package luar
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func getExportedName(name string) string {
|
||||
buf := []byte(name)
|
||||
first, n := utf8.DecodeRune(buf)
|
||||
if n == 0 {
|
||||
return name
|
||||
}
|
||||
return string(unicode.ToUpper(first)) + string(buf[n:])
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
Copyright (C) 2012 Rob Figueiredo
|
||||
All Rights Reserved.
|
||||
|
||||
MIT LICENSE
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1 @@
|
|||
[![GoDoc](http://godoc.org/github.com/robfig/cron?status.png)](http://godoc.org/github.com/robfig/cron)
|
|
@ -0,0 +1,27 @@
|
|||
package cron
|
||||
|
||||
import "time"
|
||||
|
||||
// ConstantDelaySchedule represents a simple recurring duty cycle, e.g. "Every 5 minutes".
|
||||
// It does not support jobs more frequent than once a second.
|
||||
type ConstantDelaySchedule struct {
|
||||
Delay time.Duration
|
||||
}
|
||||
|
||||
// Every returns a crontab Schedule that activates once every duration.
|
||||
// Delays of less than a second are not supported (will round up to 1 second).
|
||||
// Any fields less than a Second are truncated.
|
||||
func Every(duration time.Duration) ConstantDelaySchedule {
|
||||
if duration < time.Second {
|
||||
duration = time.Second
|
||||
}
|
||||
return ConstantDelaySchedule{
|
||||
Delay: duration - time.Duration(duration.Nanoseconds())%time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// Next returns the next time this should be run.
|
||||
// This rounds so that the next activation time will be on the second.
|
||||
func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time {
|
||||
return t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond)
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package cron
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConstantDelayNext(t *testing.T) {
|
||||
tests := []struct {
|
||||
time string
|
||||
delay time.Duration
|
||||
expected string
|
||||
}{
|
||||
// Simple cases
|
||||
{"Mon Jul 9 14:45 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"},
|
||||
{"Mon Jul 9 14:59 2012", 15 * time.Minute, "Mon Jul 9 15:14 2012"},
|
||||
{"Mon Jul 9 14:59:59 2012", 15 * time.Minute, "Mon Jul 9 15:14:59 2012"},
|
||||
|
||||
// Wrap around hours
|
||||
{"Mon Jul 9 15:45 2012", 35 * time.Minute, "Mon Jul 9 16:20 2012"},
|
||||
|
||||
// Wrap around days
|
||||
{"Mon Jul 9 23:46 2012", 14 * time.Minute, "Tue Jul 10 00:00 2012"},
|
||||
{"Mon Jul 9 23:45 2012", 35 * time.Minute, "Tue Jul 10 00:20 2012"},
|
||||
{"Mon Jul 9 23:35:51 2012", 44*time.Minute + 24*time.Second, "Tue Jul 10 00:20:15 2012"},
|
||||
{"Mon Jul 9 23:35:51 2012", 25*time.Hour + 44*time.Minute + 24*time.Second, "Thu Jul 11 01:20:15 2012"},
|
||||
|
||||
// Wrap around months
|
||||
{"Mon Jul 9 23:35 2012", 91*24*time.Hour + 25*time.Minute, "Thu Oct 9 00:00 2012"},
|
||||
|
||||
// Wrap around minute, hour, day, month, and year
|
||||
{"Mon Dec 31 23:59:45 2012", 15 * time.Second, "Tue Jan 1 00:00:00 2013"},
|
||||
|
||||
// Round to nearest second on the delay
|
||||
{"Mon Jul 9 14:45 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"},
|
||||
|
||||
// Round up to 1 second if the duration is less.
|
||||
{"Mon Jul 9 14:45:00 2012", 15 * time.Millisecond, "Mon Jul 9 14:45:01 2012"},
|
||||
|
||||
// Round to nearest second when calculating the next time.
|
||||
{"Mon Jul 9 14:45:00.005 2012", 15 * time.Minute, "Mon Jul 9 15:00 2012"},
|
||||
|
||||
// Round to nearest second for both.
|
||||
{"Mon Jul 9 14:45:00.005 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"},
|
||||
}
|
||||
|
||||
for _, c := range tests {
|
||||
actual := Every(c.delay).Next(getTime(c.time))
|
||||
expected := getTime(c.expected)
|
||||
if actual != expected {
|
||||
t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.delay, expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
// This library implements a cron spec parser and runner. See the README for
|
||||
// more details.
|
||||
package cron
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cron keeps track of any number of entries, invoking the associated func as
|
||||
// specified by the schedule. It may be started, stopped, and the entries may
|
||||
// be inspected while running.
|
||||
type Cron struct {
|
||||
entries []*Entry
|
||||
stop chan struct{}
|
||||
add chan *Entry
|
||||
snapshot chan []*Entry
|
||||
running bool
|
||||
}
|
||||
|
||||
// Job is an interface for submitted cron jobs.
|
||||
type Job interface {
|
||||
Run()
|
||||
}
|
||||
|
||||
// The Schedule describes a job's duty cycle.
|
||||
type Schedule interface {
|
||||
// Return the next activation time, later than the given time.
|
||||
// Next is invoked initially, and then each time the job is run.
|
||||
Next(time.Time) time.Time
|
||||
}
|
||||
|
||||
// Entry consists of a schedule and the func to execute on that schedule.
|
||||
type Entry struct {
|
||||
// The schedule on which this job should be run.
|
||||
Schedule Schedule
|
||||
|
||||
// The next time the job will run. This is the zero time if Cron has not been
|
||||
// started or this entry's schedule is unsatisfiable
|
||||
Next time.Time
|
||||
|
||||
// The last time this job was run. This is the zero time if the job has never
|
||||
// been run.
|
||||
Prev time.Time
|
||||
|
||||
// The Job to run.
|
||||
Job Job
|
||||
}
|
||||
|
||||
// byTime is a wrapper for sorting the entry array by time
|
||||
// (with zero time at the end).
|
||||
type byTime []*Entry
|
||||
|
||||
func (s byTime) Len() int { return len(s) }
|
||||
func (s byTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s byTime) Less(i, j int) bool {
|
||||
// Two zero times should return false.
|
||||
// Otherwise, zero is "greater" than any other time.
|
||||
// (To sort it at the end of the list.)
|
||||
if s[i].Next.IsZero() {
|
||||
return false
|
||||
}
|
||||
if s[j].Next.IsZero() {
|
||||
return true
|
||||
}
|
||||
return s[i].Next.Before(s[j].Next)
|
||||
}
|
||||
|
||||
// New returns a new Cron job runner.
|
||||
func New() *Cron {
|
||||
return &Cron{
|
||||
entries: nil,
|
||||
add: make(chan *Entry),
|
||||
stop: make(chan struct{}),
|
||||
snapshot: make(chan []*Entry),
|
||||
running: false,
|
||||
}
|
||||
}
|
||||
|
||||
// A wrapper that turns a func() into a cron.Job
|
||||
type FuncJob func()
|
||||
|
||||
func (f FuncJob) Run() { f() }
|
||||
|
||||
// AddFunc adds a func to the Cron to be run on the given schedule.
|
||||
func (c *Cron) AddFunc(spec string, cmd func()) error {
|
||||
return c.AddJob(spec, FuncJob(cmd))
|
||||
}
|
||||
|
||||
// AddFunc adds a Job to the Cron to be run on the given schedule.
|
||||
func (c *Cron) AddJob(spec string, cmd Job) error {
|
||||
schedule, err := Parse(spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Schedule(schedule, cmd)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Schedule adds a Job to the Cron to be run on the given schedule.
|
||||
func (c *Cron) Schedule(schedule Schedule, cmd Job) {
|
||||
entry := &Entry{
|
||||
Schedule: schedule,
|
||||
Job: cmd,
|
||||
}
|
||||
if !c.running {
|
||||
c.entries = append(c.entries, entry)
|
||||
return
|
||||
}
|
||||
|
||||
c.add <- entry
|
||||
}
|
||||
|
||||
// Entries returns a snapshot of the cron entries.
|
||||
func (c *Cron) Entries() []*Entry {
|
||||
if c.running {
|
||||
c.snapshot <- nil
|
||||
x := <-c.snapshot
|
||||
return x
|
||||
}
|
||||
return c.entrySnapshot()
|
||||
}
|
||||
|
||||
// Start the cron scheduler in its own go-routine.
|
||||
func (c *Cron) Start() {
|
||||
c.running = true
|
||||
go c.run()
|
||||
}
|
||||
|
||||
// Run the scheduler.. this is private just due to the need to synchronize
|
||||
// access to the 'running' state variable.
|
||||
func (c *Cron) run() {
|
||||
// Figure out the next activation times for each entry.
|
||||
now := time.Now().Local()
|
||||
for _, entry := range c.entries {
|
||||
entry.Next = entry.Schedule.Next(now)
|
||||
}
|
||||
|
||||
for {
|
||||
// Determine the next entry to run.
|
||||
sort.Sort(byTime(c.entries))
|
||||
|
||||
var effective time.Time
|
||||
if len(c.entries) == 0 || c.entries[0].Next.IsZero() {
|
||||
// If there are no entries yet, just sleep - it still handles new entries
|
||||
// and stop requests.
|
||||
effective = now.AddDate(10, 0, 0)
|
||||
} else {
|
||||
effective = c.entries[0].Next
|
||||
}
|
||||
|
||||
select {
|
||||
case now = <-time.After(effective.Sub(now)):
|
||||
// Run every entry whose next time was this effective time.
|
||||
for _, e := range c.entries {
|
||||
if e.Next != effective {
|
||||
break
|
||||
}
|
||||
go e.Job.Run()
|
||||
e.Prev = e.Next
|
||||
e.Next = e.Schedule.Next(effective)
|
||||
}
|
||||
continue
|
||||
|
||||
case newEntry := <-c.add:
|
||||
c.entries = append(c.entries, newEntry)
|
||||
newEntry.Next = newEntry.Schedule.Next(now)
|
||||
|
||||
case <-c.snapshot:
|
||||
c.snapshot <- c.entrySnapshot()
|
||||
|
||||
case <-c.stop:
|
||||
return
|
||||
}
|
||||
|
||||
// 'now' should be updated after newEntry and snapshot cases.
|
||||
now = time.Now().Local()
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the cron scheduler.
|
||||
func (c *Cron) Stop() {
|
||||
c.stop <- struct{}{}
|
||||
c.running = false
|
||||
}
|
||||
|
||||
// entrySnapshot returns a copy of the current cron entry list.
|
||||
func (c *Cron) entrySnapshot() []*Entry {
|
||||
entries := []*Entry{}
|
||||
for _, e := range c.entries {
|
||||
entries = append(entries, &Entry{
|
||||
Schedule: e.Schedule,
|
||||
Next: e.Next,
|
||||
Prev: e.Prev,
|
||||
Job: e.Job,
|
||||
})
|
||||
}
|
||||
return entries
|
||||
}
|
|
@ -0,0 +1,255 @@
|
|||
package cron
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Many tests schedule a job for every second, and then wait at most a second
|
||||
// for it to run. This amount is just slightly larger than 1 second to
|
||||
// compensate for a few milliseconds of runtime.
|
||||
const ONE_SECOND = 1*time.Second + 10*time.Millisecond
|
||||
|
||||
// Start and stop cron with no entries.
|
||||
func TestNoEntries(t *testing.T) {
|
||||
cron := New()
|
||||
cron.Start()
|
||||
|
||||
select {
|
||||
case <-time.After(ONE_SECOND):
|
||||
t.FailNow()
|
||||
case <-stop(cron):
|
||||
}
|
||||
}
|
||||
|
||||
// Start, stop, then add an entry. Verify entry doesn't run.
|
||||
func TestStopCausesJobsToNotRun(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
cron := New()
|
||||
cron.Start()
|
||||
cron.Stop()
|
||||
cron.AddFunc("* * * * * ?", func() { wg.Done() })
|
||||
|
||||
select {
|
||||
case <-time.After(ONE_SECOND):
|
||||
// No job ran!
|
||||
case <-wait(wg):
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
// Add a job, start cron, expect it runs.
|
||||
func TestAddBeforeRunning(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
cron := New()
|
||||
cron.AddFunc("* * * * * ?", func() { wg.Done() })
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
|
||||
// Give cron 2 seconds to run our job (which is always activated).
|
||||
select {
|
||||
case <-time.After(ONE_SECOND):
|
||||
t.FailNow()
|
||||
case <-wait(wg):
|
||||
}
|
||||
}
|
||||
|
||||
// Start cron, add a job, expect it runs.
|
||||
func TestAddWhileRunning(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
cron := New()
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
cron.AddFunc("* * * * * ?", func() { wg.Done() })
|
||||
|
||||
select {
|
||||
case <-time.After(ONE_SECOND):
|
||||
t.FailNow()
|
||||
case <-wait(wg):
|
||||
}
|
||||
}
|
||||
|
||||
// Test timing with Entries.
|
||||
func TestSnapshotEntries(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
cron := New()
|
||||
cron.AddFunc("@every 2s", func() { wg.Done() })
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
|
||||
// Cron should fire in 2 seconds. After 1 second, call Entries.
|
||||
select {
|
||||
case <-time.After(ONE_SECOND):
|
||||
cron.Entries()
|
||||
}
|
||||
|
||||
// Even though Entries was called, the cron should fire at the 2 second mark.
|
||||
select {
|
||||
case <-time.After(ONE_SECOND):
|
||||
t.FailNow()
|
||||
case <-wait(wg):
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Test that the entries are correctly sorted.
|
||||
// Add a bunch of long-in-the-future entries, and an immediate entry, and ensure
|
||||
// that the immediate entry runs immediately.
|
||||
// Also: Test that multiple jobs run in the same instant.
|
||||
func TestMultipleEntries(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
cron := New()
|
||||
cron.AddFunc("0 0 0 1 1 ?", func() {})
|
||||
cron.AddFunc("* * * * * ?", func() { wg.Done() })
|
||||
cron.AddFunc("0 0 0 31 12 ?", func() {})
|
||||
cron.AddFunc("* * * * * ?", func() { wg.Done() })
|
||||
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
|
||||
select {
|
||||
case <-time.After(ONE_SECOND):
|
||||
t.FailNow()
|
||||
case <-wait(wg):
|
||||
}
|
||||
}
|
||||
|
||||
// Test running the same job twice.
|
||||
func TestRunningJobTwice(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
cron := New()
|
||||
cron.AddFunc("0 0 0 1 1 ?", func() {})
|
||||
cron.AddFunc("0 0 0 31 12 ?", func() {})
|
||||
cron.AddFunc("* * * * * ?", func() { wg.Done() })
|
||||
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
|
||||
select {
|
||||
case <-time.After(2 * ONE_SECOND):
|
||||
t.FailNow()
|
||||
case <-wait(wg):
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunningMultipleSchedules(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
cron := New()
|
||||
cron.AddFunc("0 0 0 1 1 ?", func() {})
|
||||
cron.AddFunc("0 0 0 31 12 ?", func() {})
|
||||
cron.AddFunc("* * * * * ?", func() { wg.Done() })
|
||||
cron.Schedule(Every(time.Minute), FuncJob(func() {}))
|
||||
cron.Schedule(Every(time.Second), FuncJob(func() { wg.Done() }))
|
||||
cron.Schedule(Every(time.Hour), FuncJob(func() {}))
|
||||
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
|
||||
select {
|
||||
case <-time.After(2 * ONE_SECOND):
|
||||
t.FailNow()
|
||||
case <-wait(wg):
|
||||
}
|
||||
}
|
||||
|
||||
// Test that the cron is run in the local time zone (as opposed to UTC).
|
||||
func TestLocalTimezone(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
now := time.Now().Local()
|
||||
spec := fmt.Sprintf("%d %d %d %d %d ?",
|
||||
now.Second()+1, now.Minute(), now.Hour(), now.Day(), now.Month())
|
||||
|
||||
cron := New()
|
||||
cron.AddFunc(spec, func() { wg.Done() })
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
|
||||
select {
|
||||
case <-time.After(ONE_SECOND):
|
||||
t.FailNow()
|
||||
case <-wait(wg):
|
||||
}
|
||||
}
|
||||
|
||||
type testJob struct {
|
||||
wg *sync.WaitGroup
|
||||
name string
|
||||
}
|
||||
|
||||
func (t testJob) Run() {
|
||||
t.wg.Done()
|
||||
}
|
||||
|
||||
// Simple test using Runnables.
|
||||
func TestJob(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
cron := New()
|
||||
cron.AddJob("0 0 0 30 Feb ?", testJob{wg, "job0"})
|
||||
cron.AddJob("0 0 0 1 1 ?", testJob{wg, "job1"})
|
||||
cron.AddJob("* * * * * ?", testJob{wg, "job2"})
|
||||
cron.AddJob("1 0 0 1 1 ?", testJob{wg, "job3"})
|
||||
cron.Schedule(Every(5*time.Second+5*time.Nanosecond), testJob{wg, "job4"})
|
||||
cron.Schedule(Every(5*time.Minute), testJob{wg, "job5"})
|
||||
|
||||
cron.Start()
|
||||
defer cron.Stop()
|
||||
|
||||
select {
|
||||
case <-time.After(ONE_SECOND):
|
||||
t.FailNow()
|
||||
case <-wait(wg):
|
||||
}
|
||||
|
||||
// Ensure the entries are in the right order.
|
||||
expecteds := []string{"job2", "job4", "job5", "job1", "job3", "job0"}
|
||||
|
||||
var actuals []string
|
||||
for _, entry := range cron.Entries() {
|
||||
actuals = append(actuals, entry.Job.(testJob).name)
|
||||
}
|
||||
|
||||
for i, expected := range expecteds {
|
||||
if actuals[i] != expected {
|
||||
t.Errorf("Jobs not in the right order. (expected) %s != %s (actual)", expecteds, actuals)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func wait(wg *sync.WaitGroup) chan bool {
|
||||
ch := make(chan bool)
|
||||
go func() {
|
||||
wg.Wait()
|
||||
ch <- true
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
func stop(cron *Cron) chan bool {
|
||||
ch := make(chan bool)
|
||||
go func() {
|
||||
cron.Stop()
|
||||
ch <- true
|
||||
}()
|
||||
return ch
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
Package cron implements a cron spec parser and job runner.
|
||||
|
||||
Usage
|
||||
|
||||
Callers may register Funcs to be invoked on a given schedule. Cron will run
|
||||
them in their own goroutines.
|
||||
|
||||
c := cron.New()
|
||||
c.AddFunc("0 30 * * * *", func() { fmt.Println("Every hour on the half hour") })
|
||||
c.AddFunc("@hourly", func() { fmt.Println("Every hour") })
|
||||
c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty") })
|
||||
c.Start()
|
||||
..
|
||||
// Funcs are invoked in their own goroutine, asynchronously.
|
||||
...
|
||||
// Funcs may also be added to a running Cron
|
||||
c.AddFunc("@daily", func() { fmt.Println("Every day") })
|
||||
..
|
||||
// Inspect the cron job entries' next and previous run times.
|
||||
inspect(c.Entries())
|
||||
..
|
||||
c.Stop() // Stop the scheduler (does not stop any jobs already running).
|
||||
|
||||
CRON Expression Format
|
||||
|
||||
A cron expression represents a set of times, using 6 space-separated fields.
|
||||
|
||||
Field name | Mandatory? | Allowed values | Allowed special characters
|
||||
---------- | ---------- | -------------- | --------------------------
|
||||
Seconds | Yes | 0-59 | * / , -
|
||||
Minutes | Yes | 0-59 | * / , -
|
||||
Hours | Yes | 0-23 | * / , -
|
||||
Day of month | Yes | 1-31 | * / , - ?
|
||||
Month | Yes | 1-12 or JAN-DEC | * / , -
|
||||
Day of week | Yes | 0-6 or SUN-SAT | * / , - ?
|
||||
|
||||
Note: Month and Day-of-week field values are case insensitive. "SUN", "Sun",
|
||||
and "sun" are equally accepted.
|
||||
|
||||
Special Characters
|
||||
|
||||
Asterisk ( * )
|
||||
|
||||
The asterisk indicates that the cron expression will match for all values of the
|
||||
field; e.g., using an asterisk in the 5th field (month) would indicate every
|
||||
month.
|
||||
|
||||
Slash ( / )
|
||||
|
||||
Slashes are used to describe increments of ranges. For example 3-59/15 in the
|
||||
1st field (minutes) would indicate the 3rd minute of the hour and every 15
|
||||
minutes thereafter. The form "*\/..." is equivalent to the form "first-last/...",
|
||||
that is, an increment over the largest possible range of the field. The form
|
||||
"N/..." is accepted as meaning "N-MAX/...", that is, starting at N, use the
|
||||
increment until the end of that specific range. It does not wrap around.
|
||||
|
||||
Comma ( , )
|
||||
|
||||
Commas are used to separate items of a list. For example, using "MON,WED,FRI" in
|
||||
the 5th field (day of week) would mean Mondays, Wednesdays and Fridays.
|
||||
|
||||
Hyphen ( - )
|
||||
|
||||
Hyphens are used to define ranges. For example, 9-17 would indicate every
|
||||
hour between 9am and 5pm inclusive.
|
||||
|
||||
Question mark ( ? )
|
||||
|
||||
Question mark may be used instead of '*' for leaving either day-of-month or
|
||||
day-of-week blank.
|
||||
|
||||
Predefined schedules
|
||||
|
||||
You may use one of several pre-defined schedules in place of a cron expression.
|
||||
|
||||
Entry | Description | Equivalent To
|
||||
----- | ----------- | -------------
|
||||
@yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 *
|
||||
@monthly | Run once a month, midnight, first of month | 0 0 0 1 * *
|
||||
@weekly | Run once a week, midnight on Sunday | 0 0 0 * * 0
|
||||
@daily (or @midnight) | Run once a day, midnight | 0 0 0 * * *
|
||||
@hourly | Run once an hour, beginning of hour | 0 0 * * * *
|
||||
|
||||
Intervals
|
||||
|
||||
You may also schedule a job to execute at fixed intervals. This is supported by
|
||||
formatting the cron spec like this:
|
||||
|
||||
@every <duration>
|
||||
|
||||
where "duration" is a string accepted by time.ParseDuration
|
||||
(http://golang.org/pkg/time/#ParseDuration).
|
||||
|
||||
For example, "@every 1h30m10s" would indicate a schedule that activates every
|
||||
1 hour, 30 minutes, 10 seconds.
|
||||
|
||||
Note: The interval does not take the job runtime into account. For example,
|
||||
if a job takes 3 minutes to run, and it is scheduled to run every 5 minutes,
|
||||
it will have only 2 minutes of idle time between each run.
|
||||
|
||||
Time zones
|
||||
|
||||
All interpretation and scheduling is done in the machine's local time zone (as
|
||||
provided by the Go time package (http://www.golang.org/pkg/time).
|
||||
|
||||
Be aware that jobs scheduled during daylight-savings leap-ahead transitions will
|
||||
not be run!
|
||||
|
||||
Thread safety
|
||||
|
||||
Since the Cron service runs concurrently with the calling code, some amount of
|
||||
care must be taken to ensure proper synchronization.
|
||||
|
||||
All cron methods are designed to be correctly synchronized as long as the caller
|
||||
ensures that invocations have a clear happens-before ordering between them.
|
||||
|
||||
Implementation
|
||||
|
||||
Cron entries are stored in an array, sorted by their next activation time. Cron
|
||||
sleeps until the next job is due to be run.
|
||||
|
||||
Upon waking:
|
||||
- it runs each entry that is active on that second
|
||||
- it calculates the next run times for the jobs that were run
|
||||
- it re-sorts the array of entries by next activation time.
|
||||
- it goes to sleep until the soonest job.
|
||||
*/
|
||||
package cron
|
|
@ -0,0 +1,231 @@
|
|||
package cron
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Parse returns a new crontab schedule representing the given spec.
|
||||
// It returns a descriptive error if the spec is not valid.
|
||||
//
|
||||
// It accepts
|
||||
// - Full crontab specs, e.g. "* * * * * ?"
|
||||
// - Descriptors, e.g. "@midnight", "@every 1h30m"
|
||||
func Parse(spec string) (_ Schedule, err error) {
|
||||
// Convert panics into errors
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
err = fmt.Errorf("%v", recovered)
|
||||
}
|
||||
}()
|
||||
|
||||
if spec[0] == '@' {
|
||||
return parseDescriptor(spec), nil
|
||||
}
|
||||
|
||||
// Split on whitespace. We require 5 or 6 fields.
|
||||
// (second) (minute) (hour) (day of month) (month) (day of week, optional)
|
||||
fields := strings.Fields(spec)
|
||||
if len(fields) != 5 && len(fields) != 6 {
|
||||
log.Panicf("Expected 5 or 6 fields, found %d: %s", len(fields), spec)
|
||||
}
|
||||
|
||||
// If a sixth field is not provided (DayOfWeek), then it is equivalent to star.
|
||||
if len(fields) == 5 {
|
||||
fields = append(fields, "*")
|
||||
}
|
||||
|
||||
schedule := &SpecSchedule{
|
||||
Second: getField(fields[0], seconds),
|
||||
Minute: getField(fields[1], minutes),
|
||||
Hour: getField(fields[2], hours),
|
||||
Dom: getField(fields[3], dom),
|
||||
Month: getField(fields[4], months),
|
||||
Dow: getField(fields[5], dow),
|
||||
}
|
||||
|
||||
return schedule, nil
|
||||
}
|
||||
|
||||
// getField returns an Int with the bits set representing all of the times that
|
||||
// the field represents. A "field" is a comma-separated list of "ranges".
|
||||
func getField(field string, r bounds) uint64 {
|
||||
// list = range {"," range}
|
||||
var bits uint64
|
||||
ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
|
||||
for _, expr := range ranges {
|
||||
bits |= getRange(expr, r)
|
||||
}
|
||||
return bits
|
||||
}
|
||||
|
||||
// getRange returns the bits indicated by the given expression:
|
||||
// number | number "-" number [ "/" number ]
|
||||
func getRange(expr string, r bounds) uint64 {
|
||||
|
||||
var (
|
||||
start, end, step uint
|
||||
rangeAndStep = strings.Split(expr, "/")
|
||||
lowAndHigh = strings.Split(rangeAndStep[0], "-")
|
||||
singleDigit = len(lowAndHigh) == 1
|
||||
)
|
||||
|
||||
var extra_star uint64
|
||||
if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
|
||||
start = r.min
|
||||
end = r.max
|
||||
extra_star = starBit
|
||||
} else {
|
||||
start = parseIntOrName(lowAndHigh[0], r.names)
|
||||
switch len(lowAndHigh) {
|
||||
case 1:
|
||||
end = start
|
||||
case 2:
|
||||
end = parseIntOrName(lowAndHigh[1], r.names)
|
||||
default:
|
||||
log.Panicf("Too many hyphens: %s", expr)
|
||||
}
|
||||
}
|
||||
|
||||
switch len(rangeAndStep) {
|
||||
case 1:
|
||||
step = 1
|
||||
case 2:
|
||||
step = mustParseInt(rangeAndStep[1])
|
||||
|
||||
// Special handling: "N/step" means "N-max/step".
|
||||
if singleDigit {
|
||||
end = r.max
|
||||
}
|
||||
default:
|
||||
log.Panicf("Too many slashes: %s", expr)
|
||||
}
|
||||
|
||||
if start < r.min {
|
||||
log.Panicf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
|
||||
}
|
||||
if end > r.max {
|
||||
log.Panicf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
|
||||
}
|
||||
if start > end {
|
||||
log.Panicf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
|
||||
}
|
||||
|
||||
return getBits(start, end, step) | extra_star
|
||||
}
|
||||
|
||||
// parseIntOrName returns the (possibly-named) integer contained in expr.
|
||||
func parseIntOrName(expr string, names map[string]uint) uint {
|
||||
if names != nil {
|
||||
if namedInt, ok := names[strings.ToLower(expr)]; ok {
|
||||
return namedInt
|
||||
}
|
||||
}
|
||||
return mustParseInt(expr)
|
||||
}
|
||||
|
||||
// mustParseInt parses the given expression as an int or panics.
|
||||
func mustParseInt(expr string) uint {
|
||||
num, err := strconv.Atoi(expr)
|
||||
if err != nil {
|
||||
log.Panicf("Failed to parse int from %s: %s", expr, err)
|
||||
}
|
||||
if num < 0 {
|
||||
log.Panicf("Negative number (%d) not allowed: %s", num, expr)
|
||||
}
|
||||
|
||||
return uint(num)
|
||||
}
|
||||
|
||||
// getBits sets all bits in the range [min, max], modulo the given step size.
|
||||
func getBits(min, max, step uint) uint64 {
|
||||
var bits uint64
|
||||
|
||||
// If step is 1, use shifts.
|
||||
if step == 1 {
|
||||
return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
|
||||
}
|
||||
|
||||
// Else, use a simple loop.
|
||||
for i := min; i <= max; i += step {
|
||||
bits |= 1 << i
|
||||
}
|
||||
return bits
|
||||
}
|
||||
|
||||
// all returns all bits within the given bounds. (plus the star bit)
|
||||
func all(r bounds) uint64 {
|
||||
return getBits(r.min, r.max, 1) | starBit
|
||||
}
|
||||
|
||||
// parseDescriptor returns a pre-defined schedule for the expression, or panics
|
||||
// if none matches.
|
||||
func parseDescriptor(spec string) Schedule {
|
||||
switch spec {
|
||||
case "@yearly", "@annually":
|
||||
return &SpecSchedule{
|
||||
Second: 1 << seconds.min,
|
||||
Minute: 1 << minutes.min,
|
||||
Hour: 1 << hours.min,
|
||||
Dom: 1 << dom.min,
|
||||
Month: 1 << months.min,
|
||||
Dow: all(dow),
|
||||
}
|
||||
|
||||
case "@monthly":
|
||||
return &SpecSchedule{
|
||||
Second: 1 << seconds.min,
|
||||
Minute: 1 << minutes.min,
|
||||
Hour: 1 << hours.min,
|
||||
Dom: 1 << dom.min,
|
||||
Month: all(months),
|
||||
Dow: all(dow),
|
||||
}
|
||||
|
||||
case "@weekly":
|
||||
return &SpecSchedule{
|
||||
Second: 1 << seconds.min,
|
||||
Minute: 1 << minutes.min,
|
||||
Hour: 1 << hours.min,
|
||||
Dom: all(dom),
|
||||
Month: all(months),
|
||||
Dow: 1 << dow.min,
|
||||
}
|
||||
|
||||
case "@daily", "@midnight":
|
||||
return &SpecSchedule{
|
||||
Second: 1 << seconds.min,
|
||||
Minute: 1 << minutes.min,
|
||||
Hour: 1 << hours.min,
|
||||
Dom: all(dom),
|
||||
Month: all(months),
|
||||
Dow: all(dow),
|
||||
}
|
||||
|
||||
case "@hourly":
|
||||
return &SpecSchedule{
|
||||
Second: 1 << seconds.min,
|
||||
Minute: 1 << minutes.min,
|
||||
Hour: all(hours),
|
||||
Dom: all(dom),
|
||||
Month: all(months),
|
||||
Dow: all(dow),
|
||||
}
|
||||
}
|
||||
|
||||
const every = "@every "
|
||||
if strings.HasPrefix(spec, every) {
|
||||
duration, err := time.ParseDuration(spec[len(every):])
|
||||
if err != nil {
|
||||
log.Panicf("Failed to parse duration %s: %s", spec, err)
|
||||
}
|
||||
return Every(duration)
|
||||
}
|
||||
|
||||
log.Panicf("Unrecognized descriptor: %s", spec)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
package cron
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRange(t *testing.T) {
|
||||
ranges := []struct {
|
||||
expr string
|
||||
min, max uint
|
||||
expected uint64
|
||||
}{
|
||||
{"5", 0, 7, 1 << 5},
|
||||
{"0", 0, 7, 1 << 0},
|
||||
{"7", 0, 7, 1 << 7},
|
||||
|
||||
{"5-5", 0, 7, 1 << 5},
|
||||
{"5-6", 0, 7, 1<<5 | 1<<6},
|
||||
{"5-7", 0, 7, 1<<5 | 1<<6 | 1<<7},
|
||||
|
||||
{"5-6/2", 0, 7, 1 << 5},
|
||||
{"5-7/2", 0, 7, 1<<5 | 1<<7},
|
||||
{"5-7/1", 0, 7, 1<<5 | 1<<6 | 1<<7},
|
||||
|
||||
{"*", 1, 3, 1<<1 | 1<<2 | 1<<3 | starBit},
|
||||
{"*/2", 1, 3, 1<<1 | 1<<3 | starBit},
|
||||
}
|
||||
|
||||
for _, c := range ranges {
|
||||
actual := getRange(c.expr, bounds{c.min, c.max, nil})
|
||||
if actual != c.expected {
|
||||
t.Errorf("%s => (expected) %d != %d (actual)", c.expr, c.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestField(t *testing.T) {
|
||||
fields := []struct {
|
||||
expr string
|
||||
min, max uint
|
||||
expected uint64
|
||||
}{
|
||||
{"5", 1, 7, 1 << 5},
|
||||
{"5,6", 1, 7, 1<<5 | 1<<6},
|
||||
{"5,6,7", 1, 7, 1<<5 | 1<<6 | 1<<7},
|
||||
{"1,5-7/2,3", 1, 7, 1<<1 | 1<<5 | 1<<7 | 1<<3},
|
||||
}
|
||||
|
||||
for _, c := range fields {
|
||||
actual := getField(c.expr, bounds{c.min, c.max, nil})
|
||||
if actual != c.expected {
|
||||
t.Errorf("%s => (expected) %d != %d (actual)", c.expr, c.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBits(t *testing.T) {
|
||||
allBits := []struct {
|
||||
r bounds
|
||||
expected uint64
|
||||
}{
|
||||
{minutes, 0xfffffffffffffff}, // 0-59: 60 ones
|
||||
{hours, 0xffffff}, // 0-23: 24 ones
|
||||
{dom, 0xfffffffe}, // 1-31: 31 ones, 1 zero
|
||||
{months, 0x1ffe}, // 1-12: 12 ones, 1 zero
|
||||
{dow, 0x7f}, // 0-6: 7 ones
|
||||
}
|
||||
|
||||
for _, c := range allBits {
|
||||
actual := all(c.r) // all() adds the starBit, so compensate for that..
|
||||
if c.expected|starBit != actual {
|
||||
t.Errorf("%d-%d/%d => (expected) %b != %b (actual)",
|
||||
c.r.min, c.r.max, 1, c.expected|starBit, actual)
|
||||
}
|
||||
}
|
||||
|
||||
bits := []struct {
|
||||
min, max, step uint
|
||||
expected uint64
|
||||
}{
|
||||
|
||||
{0, 0, 1, 0x1},
|
||||
{1, 1, 1, 0x2},
|
||||
{1, 5, 2, 0x2a}, // 101010
|
||||
{1, 4, 2, 0xa}, // 1010
|
||||
}
|
||||
|
||||
for _, c := range bits {
|
||||
actual := getBits(c.min, c.max, c.step)
|
||||
if c.expected != actual {
|
||||
t.Errorf("%d-%d/%d => (expected) %b != %b (actual)",
|
||||
c.min, c.max, c.step, c.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecSchedule(t *testing.T) {
|
||||
entries := []struct {
|
||||
expr string
|
||||
expected Schedule
|
||||
}{
|
||||
{"* 5 * * * *", &SpecSchedule{all(seconds), 1 << 5, all(hours), all(dom), all(months), all(dow)}},
|
||||
{"@every 5m", ConstantDelaySchedule{time.Duration(5) * time.Minute}},
|
||||
}
|
||||
|
||||
for _, c := range entries {
|
||||
actual, err := Parse(c.expr)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if !reflect.DeepEqual(actual, c.expected) {
|
||||
t.Errorf("%s => (expected) %b != %b (actual)", c.expr, c.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
package cron
|
||||
|
||||
import "time"
|
||||
|
||||
// SpecSchedule specifies a duty cycle (to the second granularity), based on a
|
||||
// traditional crontab specification. It is computed initially and stored as bit sets.
|
||||
type SpecSchedule struct {
|
||||
Second, Minute, Hour, Dom, Month, Dow uint64
|
||||
}
|
||||
|
||||
// bounds provides a range of acceptable values (plus a map of name to value).
|
||||
type bounds struct {
|
||||
min, max uint
|
||||
names map[string]uint
|
||||
}
|
||||
|
||||
// The bounds for each field.
|
||||
var (
|
||||
seconds = bounds{0, 59, nil}
|
||||
minutes = bounds{0, 59, nil}
|
||||
hours = bounds{0, 23, nil}
|
||||
dom = bounds{1, 31, nil}
|
||||
months = bounds{1, 12, map[string]uint{
|
||||
"jan": 1,
|
||||
"feb": 2,
|
||||
"mar": 3,
|
||||
"apr": 4,
|
||||
"may": 5,
|
||||
"jun": 6,
|
||||
"jul": 7,
|
||||
"aug": 8,
|
||||
"sep": 9,
|
||||
"oct": 10,
|
||||
"nov": 11,
|
||||
"dec": 12,
|
||||
}}
|
||||
dow = bounds{0, 6, map[string]uint{
|
||||
"sun": 0,
|
||||
"mon": 1,
|
||||
"tue": 2,
|
||||
"wed": 3,
|
||||
"thu": 4,
|
||||
"fri": 5,
|
||||
"sat": 6,
|
||||
}}
|
||||
)
|
||||
|
||||
const (
|
||||
// Set the top bit if a star was included in the expression.
|
||||
starBit = 1 << 63
|
||||
)
|
||||
|
||||
// Next returns the next time this schedule is activated, greater than the given
|
||||
// time. If no time can be found to satisfy the schedule, return the zero time.
|
||||
func (s *SpecSchedule) Next(t time.Time) time.Time {
|
||||
// General approach:
|
||||
// For Month, Day, Hour, Minute, Second:
|
||||
// Check if the time value matches. If yes, continue to the next field.
|
||||
// If the field doesn't match the schedule, then increment the field until it matches.
|
||||
// While incrementing the field, a wrap-around brings it back to the beginning
|
||||
// of the field list (since it is necessary to re-verify previous field
|
||||
// values)
|
||||
|
||||
// Start at the earliest possible time (the upcoming second).
|
||||
t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond)
|
||||
|
||||
// This flag indicates whether a field has been incremented.
|
||||
added := false
|
||||
|
||||
// If no time is found within five years, return zero.
|
||||
yearLimit := t.Year() + 5
|
||||
|
||||
WRAP:
|
||||
if t.Year() > yearLimit {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// Find the first applicable month.
|
||||
// If it's this month, then do nothing.
|
||||
for 1<<uint(t.Month())&s.Month == 0 {
|
||||
// If we have to add a month, reset the other parts to 0.
|
||||
if !added {
|
||||
added = true
|
||||
// Otherwise, set the date at the beginning (since the current time is irrelevant).
|
||||
t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
|
||||
}
|
||||
t = t.AddDate(0, 1, 0)
|
||||
|
||||
// Wrapped around.
|
||||
if t.Month() == time.January {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
// Now get a day in that month.
|
||||
for !dayMatches(s, t) {
|
||||
if !added {
|
||||
added = true
|
||||
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
|
||||
}
|
||||
t = t.AddDate(0, 0, 1)
|
||||
|
||||
if t.Day() == 1 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
for 1<<uint(t.Hour())&s.Hour == 0 {
|
||||
if !added {
|
||||
added = true
|
||||
t = t.Truncate(time.Hour)
|
||||
}
|
||||
t = t.Add(1 * time.Hour)
|
||||
|
||||
if t.Hour() == 0 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
for 1<<uint(t.Minute())&s.Minute == 0 {
|
||||
if !added {
|
||||
added = true
|
||||
t = t.Truncate(time.Minute)
|
||||
}
|
||||
t = t.Add(1 * time.Minute)
|
||||
|
||||
if t.Minute() == 0 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
for 1<<uint(t.Second())&s.Second == 0 {
|
||||
if !added {
|
||||
added = true
|
||||
t = t.Truncate(time.Second)
|
||||
}
|
||||
t = t.Add(1 * time.Second)
|
||||
|
||||
if t.Second() == 0 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// dayMatches returns true if the schedule's day-of-week and day-of-month
|
||||
// restrictions are satisfied by the given time.
|
||||
func dayMatches(s *SpecSchedule, t time.Time) bool {
|
||||
var (
|
||||
domMatch bool = 1<<uint(t.Day())&s.Dom > 0
|
||||
dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0
|
||||
)
|
||||
|
||||
if s.Dom&starBit > 0 || s.Dow&starBit > 0 {
|
||||
return domMatch && dowMatch
|
||||
}
|
||||
return domMatch || dowMatch
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
package cron
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestActivation(t *testing.T) {
|
||||
tests := []struct {
|
||||
time, spec string
|
||||
expected bool
|
||||
}{
|
||||
// Every fifteen minutes.
|
||||
{"Mon Jul 9 15:00 2012", "0 0/15 * * *", true},
|
||||
{"Mon Jul 9 15:45 2012", "0 0/15 * * *", true},
|
||||
{"Mon Jul 9 15:40 2012", "0 0/15 * * *", false},
|
||||
|
||||
// Every fifteen minutes, starting at 5 minutes.
|
||||
{"Mon Jul 9 15:05 2012", "0 5/15 * * *", true},
|
||||
{"Mon Jul 9 15:20 2012", "0 5/15 * * *", true},
|
||||
{"Mon Jul 9 15:50 2012", "0 5/15 * * *", true},
|
||||
|
||||
// Named months
|
||||
{"Sun Jul 15 15:00 2012", "0 0/15 * * Jul", true},
|
||||
{"Sun Jul 15 15:00 2012", "0 0/15 * * Jun", false},
|
||||
|
||||
// Everything set.
|
||||
{"Sun Jul 15 08:30 2012", "0 30 08 ? Jul Sun", true},
|
||||
{"Sun Jul 15 08:30 2012", "0 30 08 15 Jul ?", true},
|
||||
{"Mon Jul 16 08:30 2012", "0 30 08 ? Jul Sun", false},
|
||||
{"Mon Jul 16 08:30 2012", "0 30 08 15 Jul ?", false},
|
||||
|
||||
// Predefined schedules
|
||||
{"Mon Jul 9 15:00 2012", "@hourly", true},
|
||||
{"Mon Jul 9 15:04 2012", "@hourly", false},
|
||||
{"Mon Jul 9 15:00 2012", "@daily", false},
|
||||
{"Mon Jul 9 00:00 2012", "@daily", true},
|
||||
{"Mon Jul 9 00:00 2012", "@weekly", false},
|
||||
{"Sun Jul 8 00:00 2012", "@weekly", true},
|
||||
{"Sun Jul 8 01:00 2012", "@weekly", false},
|
||||
{"Sun Jul 8 00:00 2012", "@monthly", false},
|
||||
{"Sun Jul 1 00:00 2012", "@monthly", true},
|
||||
|
||||
// Test interaction of DOW and DOM.
|
||||
// If both are specified, then only one needs to match.
|
||||
{"Sun Jul 15 00:00 2012", "0 * * 1,15 * Sun", true},
|
||||
{"Fri Jun 15 00:00 2012", "0 * * 1,15 * Sun", true},
|
||||
{"Wed Aug 1 00:00 2012", "0 * * 1,15 * Sun", true},
|
||||
|
||||
// However, if one has a star, then both need to match.
|
||||
{"Sun Jul 15 00:00 2012", "0 * * * * Mon", false},
|
||||
{"Sun Jul 15 00:00 2012", "0 * * */10 * Sun", false},
|
||||
{"Mon Jul 9 00:00 2012", "0 * * 1,15 * *", false},
|
||||
{"Sun Jul 15 00:00 2012", "0 * * 1,15 * *", true},
|
||||
{"Sun Jul 15 00:00 2012", "0 * * */2 * Sun", true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
sched, err := Parse(test.spec)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
actual := sched.Next(getTime(test.time).Add(-1 * time.Second))
|
||||
expected := getTime(test.time)
|
||||
if test.expected && expected != actual || !test.expected && expected == actual {
|
||||
t.Errorf("Fail evaluating %s on %s: (expected) %s != %s (actual)",
|
||||
test.spec, test.time, expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNext(t *testing.T) {
|
||||
runs := []struct {
|
||||
time, spec string
|
||||
expected string
|
||||
}{
|
||||
// Simple cases
|
||||
{"Mon Jul 9 14:45 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"},
|
||||
{"Mon Jul 9 14:59 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"},
|
||||
{"Mon Jul 9 14:59:59 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"},
|
||||
|
||||
// Wrap around hours
|
||||
{"Mon Jul 9 15:45 2012", "0 20-35/15 * * *", "Mon Jul 9 16:20 2012"},
|
||||
|
||||
// Wrap around days
|
||||
{"Mon Jul 9 23:46 2012", "0 */15 * * *", "Tue Jul 10 00:00 2012"},
|
||||
{"Mon Jul 9 23:45 2012", "0 20-35/15 * * *", "Tue Jul 10 00:20 2012"},
|
||||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * * *", "Tue Jul 10 00:20:15 2012"},
|
||||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 * *", "Tue Jul 10 01:20:15 2012"},
|
||||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 10-12 * *", "Tue Jul 10 10:20:15 2012"},
|
||||
|
||||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 */2 * *", "Thu Jul 11 01:20:15 2012"},
|
||||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 * *", "Wed Jul 10 00:20:15 2012"},
|
||||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 Jul *", "Wed Jul 10 00:20:15 2012"},
|
||||
|
||||
// Wrap around months
|
||||
{"Mon Jul 9 23:35 2012", "0 0 0 9 Apr-Oct ?", "Thu Aug 9 00:00 2012"},
|
||||
{"Mon Jul 9 23:35 2012", "0 0 0 */5 Apr,Aug,Oct Mon", "Mon Aug 6 00:00 2012"},
|
||||
{"Mon Jul 9 23:35 2012", "0 0 0 */5 Oct Mon", "Mon Oct 1 00:00 2012"},
|
||||
|
||||
// Wrap around years
|
||||
{"Mon Jul 9 23:35 2012", "0 0 0 * Feb Mon", "Mon Feb 4 00:00 2013"},
|
||||
{"Mon Jul 9 23:35 2012", "0 0 0 * Feb Mon/2", "Fri Feb 1 00:00 2013"},
|
||||
|
||||
// Wrap around minute, hour, day, month, and year
|
||||
{"Mon Dec 31 23:59:45 2012", "0 * * * * *", "Tue Jan 1 00:00:00 2013"},
|
||||
|
||||
// Leap year
|
||||
{"Mon Jul 9 23:35 2012", "0 0 0 29 Feb ?", "Mon Feb 29 00:00 2016"},
|
||||
|
||||
// Daylight savings time 2am EST (-5) -> 3am EDT (-4)
|
||||
{"2012-03-11T00:00:00-0500", "0 30 2 11 Mar ?", "2013-03-11T02:30:00-0400"},
|
||||
|
||||
// hourly job
|
||||
{"2012-03-11T00:00:00-0500", "0 0 * * * ?", "2012-03-11T01:00:00-0500"},
|
||||
{"2012-03-11T01:00:00-0500", "0 0 * * * ?", "2012-03-11T03:00:00-0400"},
|
||||
{"2012-03-11T03:00:00-0400", "0 0 * * * ?", "2012-03-11T04:00:00-0400"},
|
||||
{"2012-03-11T04:00:00-0400", "0 0 * * * ?", "2012-03-11T05:00:00-0400"},
|
||||
|
||||
// 1am nightly job
|
||||
{"2012-03-11T00:00:00-0500", "0 0 1 * * ?", "2012-03-11T01:00:00-0500"},
|
||||
{"2012-03-11T01:00:00-0500", "0 0 1 * * ?", "2012-03-12T01:00:00-0400"},
|
||||
|
||||
// 2am nightly job (skipped)
|
||||
{"2012-03-11T00:00:00-0500", "0 0 2 * * ?", "2012-03-12T02:00:00-0400"},
|
||||
|
||||
// Daylight savings time 2am EDT (-4) => 1am EST (-5)
|
||||
{"2012-11-04T00:00:00-0400", "0 30 2 04 Nov ?", "2012-11-04T02:30:00-0500"},
|
||||
{"2012-11-04T01:45:00-0400", "0 30 1 04 Nov ?", "2012-11-04T01:30:00-0500"},
|
||||
|
||||
// hourly job
|
||||
{"2012-11-04T00:00:00-0400", "0 0 * * * ?", "2012-11-04T01:00:00-0400"},
|
||||
{"2012-11-04T01:00:00-0400", "0 0 * * * ?", "2012-11-04T01:00:00-0500"},
|
||||
{"2012-11-04T01:00:00-0500", "0 0 * * * ?", "2012-11-04T02:00:00-0500"},
|
||||
|
||||
// 1am nightly job (runs twice)
|
||||
{"2012-11-04T00:00:00-0400", "0 0 1 * * ?", "2012-11-04T01:00:00-0400"},
|
||||
{"2012-11-04T01:00:00-0400", "0 0 1 * * ?", "2012-11-04T01:00:00-0500"},
|
||||
{"2012-11-04T01:00:00-0500", "0 0 1 * * ?", "2012-11-05T01:00:00-0500"},
|
||||
|
||||
// 2am nightly job
|
||||
{"2012-11-04T00:00:00-0400", "0 0 2 * * ?", "2012-11-04T02:00:00-0500"},
|
||||
{"2012-11-04T02:00:00-0500", "0 0 2 * * ?", "2012-11-05T02:00:00-0500"},
|
||||
|
||||
// 3am nightly job
|
||||
{"2012-11-04T00:00:00-0400", "0 0 3 * * ?", "2012-11-04T03:00:00-0500"},
|
||||
{"2012-11-04T03:00:00-0500", "0 0 3 * * ?", "2012-11-05T03:00:00-0500"},
|
||||
|
||||
// Unsatisfiable
|
||||
{"Mon Jul 9 23:35 2012", "0 0 0 30 Feb ?", ""},
|
||||
{"Mon Jul 9 23:35 2012", "0 0 0 31 Apr ?", ""},
|
||||
}
|
||||
|
||||
for _, c := range runs {
|
||||
sched, err := Parse(c.spec)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
actual := sched.Next(getTime(c.time))
|
||||
expected := getTime(c.expected)
|
||||
if !actual.Equal(expected) {
|
||||
t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.spec, expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
invalidSpecs := []string{
|
||||
"xyz",
|
||||
"60 0 * * *",
|
||||
"0 60 * * *",
|
||||
"0 0 * * XYZ",
|
||||
}
|
||||
for _, spec := range invalidSpecs {
|
||||
_, err := Parse(spec)
|
||||
if err == nil {
|
||||
t.Error("expected an error parsing: ", spec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getTime(value string) time.Time {
|
||||
if value == "" {
|
||||
return time.Time{}
|
||||
}
|
||||
t, err := time.Parse("Mon Jan 2 15:04 2006", value)
|
||||
if err != nil {
|
||||
t, err = time.Parse("Mon Jan 2 15:04:05 2006", value)
|
||||
if err != nil {
|
||||
t, err = time.Parse("2006-01-02T15:04:05-0700", value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Daylight savings time tests require location
|
||||
if ny, err := time.LoadLocation("America/New_York"); err == nil {
|
||||
t = t.In(ny)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
Copyright (c) 2012 Péter Surányi. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Portions of gcfg's source code have been derived from Go, and are
|
||||
covered by the following license:
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,7 @@
|
|||
Gcfg reads INI-style configuration files into Go structs;
|
||||
supports user-defined types and subsections.
|
||||
|
||||
Project page: https://code.google.com/p/gcfg
|
||||
Package docs: http://godoc.org/code.google.com/p/gcfg
|
||||
|
||||
My other projects: https://speter.net
|
|
@ -0,0 +1,118 @@
|
|||
// Package gcfg reads "INI-style" text-based configuration files with
|
||||
// "name=value" pairs grouped into sections (gcfg files).
|
||||
//
|
||||
// This package is still a work in progress; see the sections below for planned
|
||||
// changes.
|
||||
//
|
||||
// Syntax
|
||||
//
|
||||
// The syntax is based on that used by git config:
|
||||
// http://git-scm.com/docs/git-config#_syntax .
|
||||
// There are some (planned) differences compared to the git config format:
|
||||
// - improve data portability:
|
||||
// - must be encoded in UTF-8 (for now) and must not contain the 0 byte
|
||||
// - include and "path" type is not supported
|
||||
// (path type may be implementable as a user-defined type)
|
||||
// - internationalization
|
||||
// - section and variable names can contain unicode letters, unicode digits
|
||||
// (as defined in http://golang.org/ref/spec#Characters ) and hyphens
|
||||
// (U+002D), starting with a unicode letter
|
||||
// - disallow potentially ambiguous or misleading definitions:
|
||||
// - `[sec.sub]` format is not allowed (deprecated in gitconfig)
|
||||
// - `[sec ""]` is not allowed
|
||||
// - use `[sec]` for section name "sec" and empty subsection name
|
||||
// - (planned) within a single file, definitions must be contiguous for each:
|
||||
// - section: '[secA]' -> '[secB]' -> '[secA]' is an error
|
||||
// - subsection: '[sec "A"]' -> '[sec "B"]' -> '[sec "A"]' is an error
|
||||
// - multivalued variable: 'multi=a' -> 'other=x' -> 'multi=b' is an error
|
||||
//
|
||||
// Data structure
|
||||
//
|
||||
// The functions in this package read values into a user-defined struct.
|
||||
// Each section corresponds to a struct field in the config struct, and each
|
||||
// variable in a section corresponds to a data field in the section struct.
|
||||
// The mapping of each section or variable name to fields is done either based
|
||||
// on the "gcfg" struct tag or by matching the name of the section or variable,
|
||||
// ignoring case. In the latter case, hyphens '-' in section and variable names
|
||||
// correspond to underscores '_' in field names.
|
||||
// Fields must be exported; to use a section or variable name starting with a
|
||||
// letter that is neither upper- or lower-case, prefix the field name with 'X'.
|
||||
// (See https://code.google.com/p/go/issues/detail?id=5763#c4 .)
|
||||
//
|
||||
// For sections with subsections, the corresponding field in config must be a
|
||||
// map, rather than a struct, with string keys and pointer-to-struct values.
|
||||
// Values for subsection variables are stored in the map with the subsection
|
||||
// name used as the map key.
|
||||
// (Note that unlike section and variable names, subsection names are case
|
||||
// sensitive.)
|
||||
// When using a map, and there is a section with the same section name but
|
||||
// without a subsection name, its values are stored with the empty string used
|
||||
// as the key.
|
||||
//
|
||||
// The functions in this package panic if config is not a pointer to a struct,
|
||||
// or when a field is not of a suitable type (either a struct or a map with
|
||||
// string keys and pointer-to-struct values).
|
||||
//
|
||||
// Parsing of values
|
||||
//
|
||||
// The section structs in the config struct may contain single-valued or
|
||||
// multi-valued variables. Variables of unnamed slice type (that is, a type
|
||||
// starting with `[]`) are treated as multi-value; all others (including named
|
||||
// slice types) are treated as single-valued variables.
|
||||
//
|
||||
// Single-valued variables are handled based on the type as follows.
|
||||
// Unnamed pointer types (that is, types starting with `*`) are dereferenced,
|
||||
// and if necessary, a new instance is allocated.
|
||||
//
|
||||
// For types implementing the encoding.TextUnmarshaler interface, the
|
||||
// UnmarshalText method is used to set the value. Implementing this method is
|
||||
// the recommended way for parsing user-defined types.
|
||||
//
|
||||
// For fields of string kind, the value string is assigned to the field, after
|
||||
// unquoting and unescaping as needed.
|
||||
// For fields of bool kind, the field is set to true if the value is "true",
|
||||
// "yes", "on" or "1", and set to false if the value is "false", "no", "off" or
|
||||
// "0", ignoring case. In addition, single-valued bool fields can be specified
|
||||
// with a "blank" value (variable name without equals sign and value); in such
|
||||
// case the value is set to true.
|
||||
//
|
||||
// Predefined integer types [u]int(|8|16|32|64) and big.Int are parsed as
|
||||
// decimal or hexadecimal (if having '0x' prefix). (This is to prevent
|
||||
// unintuitively handling zero-padded numbers as octal.) Other types having
|
||||
// [u]int* as the underlying type, such as os.FileMode and uintptr allow
|
||||
// decimal, hexadecimal, or octal values.
|
||||
// Parsing mode for integer types can be overridden using the struct tag option
|
||||
// ",int=mode" where mode is a combination of the 'd', 'h', and 'o' characters
|
||||
// (each standing for decimal, hexadecimal, and octal, respectively.)
|
||||
//
|
||||
// All other types are parsed using fmt.Sscanf with the "%v" verb.
|
||||
//
|
||||
// For multi-valued variables, each individual value is parsed as above and
|
||||
// appended to the slice. If the first value is specified as a "blank" value
|
||||
// (variable name without equals sign and value), a new slice is allocated;
|
||||
// that is any values previously set in the slice will be ignored.
|
||||
//
|
||||
// The types subpackage for provides helpers for parsing "enum-like" and integer
|
||||
// types.
|
||||
//
|
||||
// TODO
|
||||
//
|
||||
// The following is a list of changes under consideration:
|
||||
// - documentation
|
||||
// - self-contained syntax documentation
|
||||
// - more practical examples
|
||||
// - move TODOs to issue tracker (eventually)
|
||||
// - syntax
|
||||
// - reconsider valid escape sequences
|
||||
// (gitconfig doesn't support \r in value, \t in subsection name, etc.)
|
||||
// - reading / parsing gcfg files
|
||||
// - define internal representation structure
|
||||
// - support multiple inputs (readers, strings, files)
|
||||
// - support declaring encoding (?)
|
||||
// - support varying fields sets for subsections (?)
|
||||
// - writing gcfg files
|
||||
// - error handling
|
||||
// - make error context accessible programmatically?
|
||||
// - limit input size?
|
||||
//
|
||||
package gcfg
|
|
@ -0,0 +1,132 @@
|
|||
package gcfg_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
import "github.com/scalingdata/gcfg"
|
||||
|
||||
func ExampleReadStringInto() {
|
||||
cfgStr := `; Comment line
|
||||
[section]
|
||||
name=value # comment`
|
||||
cfg := struct {
|
||||
Section struct {
|
||||
Name string
|
||||
}
|
||||
}{}
|
||||
err := gcfg.ReadStringInto(&cfg, cfgStr)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse gcfg data: %s", err)
|
||||
}
|
||||
fmt.Println(cfg.Section.Name)
|
||||
// Output: value
|
||||
}
|
||||
|
||||
func ExampleReadStringInto_bool() {
|
||||
cfgStr := `; Comment line
|
||||
[section]
|
||||
switch=on`
|
||||
cfg := struct {
|
||||
Section struct {
|
||||
Switch bool
|
||||
}
|
||||
}{}
|
||||
err := gcfg.ReadStringInto(&cfg, cfgStr)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse gcfg data: %s", err)
|
||||
}
|
||||
fmt.Println(cfg.Section.Switch)
|
||||
// Output: true
|
||||
}
|
||||
|
||||
func ExampleReadStringInto_hyphens() {
|
||||
cfgStr := `; Comment line
|
||||
[section-name]
|
||||
variable-name=value # comment`
|
||||
cfg := struct {
|
||||
Section_Name struct {
|
||||
Variable_Name string
|
||||
}
|
||||
}{}
|
||||
err := gcfg.ReadStringInto(&cfg, cfgStr)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse gcfg data: %s", err)
|
||||
}
|
||||
fmt.Println(cfg.Section_Name.Variable_Name)
|
||||
// Output: value
|
||||
}
|
||||
|
||||
func ExampleReadStringInto_tags() {
|
||||
cfgStr := `; Comment line
|
||||
[section]
|
||||
var-name=value # comment`
|
||||
cfg := struct {
|
||||
Section struct {
|
||||
FieldName string `gcfg:"var-name"`
|
||||
}
|
||||
}{}
|
||||
err := gcfg.ReadStringInto(&cfg, cfgStr)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse gcfg data: %s", err)
|
||||
}
|
||||
fmt.Println(cfg.Section.FieldName)
|
||||
// Output: value
|
||||
}
|
||||
|
||||
func ExampleReadStringInto_subsections() {
|
||||
cfgStr := `; Comment line
|
||||
[profile "A"]
|
||||
color = white
|
||||
|
||||
[profile "B"]
|
||||
color = black
|
||||
`
|
||||
cfg := struct {
|
||||
Profile map[string]*struct {
|
||||
Color string
|
||||
}
|
||||
}{}
|
||||
err := gcfg.ReadStringInto(&cfg, cfgStr)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse gcfg data: %s", err)
|
||||
}
|
||||
fmt.Printf("%s %s\n", cfg.Profile["A"].Color, cfg.Profile["B"].Color)
|
||||
// Output: white black
|
||||
}
|
||||
|
||||
func ExampleReadStringInto_multivalue() {
|
||||
cfgStr := `; Comment line
|
||||
[section]
|
||||
multi=value1
|
||||
multi=value2`
|
||||
cfg := struct {
|
||||
Section struct {
|
||||
Multi []string
|
||||
}
|
||||
}{}
|
||||
err := gcfg.ReadStringInto(&cfg, cfgStr)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse gcfg data: %s", err)
|
||||
}
|
||||
fmt.Println(cfg.Section.Multi)
|
||||
// Output: [value1 value2]
|
||||
}
|
||||
|
||||
func ExampleReadStringInto_unicode() {
|
||||
cfgStr := `; Comment line
|
||||
[甲]
|
||||
乙=丙 # comment`
|
||||
cfg := struct {
|
||||
X甲 struct {
|
||||
X乙 string
|
||||
}
|
||||
}{}
|
||||
err := gcfg.ReadStringInto(&cfg, cfgStr)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse gcfg data: %s", err)
|
||||
}
|
||||
fmt.Println(cfg.X甲.X乙)
|
||||
// Output: 丙
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
// +build !go1.2
|
||||
|
||||
package gcfg
|
||||
|
||||
type textUnmarshaler interface {
|
||||
UnmarshalText(text []byte) error
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
// +build go1.2
|
||||
|
||||
package gcfg
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
)
|
||||
|
||||
type textUnmarshaler encoding.TextUnmarshaler
|
|
@ -0,0 +1,63 @@
|
|||
package gcfg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type Config1 struct {
|
||||
Section struct {
|
||||
Int int
|
||||
BigInt big.Int
|
||||
}
|
||||
}
|
||||
|
||||
var testsIssue1 = []struct {
|
||||
cfg string
|
||||
typename string
|
||||
}{
|
||||
{"[section]\nint=X", "int"},
|
||||
{"[section]\nint=", "int"},
|
||||
{"[section]\nint=1A", "int"},
|
||||
{"[section]\nbigint=X", "big.Int"},
|
||||
{"[section]\nbigint=", "big.Int"},
|
||||
{"[section]\nbigint=1A", "big.Int"},
|
||||
}
|
||||
|
||||
// Value parse error should:
|
||||
// - include plain type name
|
||||
// - not include reflect internals
|
||||
func TestIssue1(t *testing.T) {
|
||||
for i, tt := range testsIssue1 {
|
||||
var c Config1
|
||||
err := ReadStringInto(&c, tt.cfg)
|
||||
switch {
|
||||
case err == nil:
|
||||
t.Errorf("%d fail: got ok; wanted error", i)
|
||||
case !strings.Contains(err.Error(), tt.typename):
|
||||
t.Errorf("%d fail: error message doesn't contain type name %q: %v",
|
||||
i, tt.typename, err)
|
||||
case strings.Contains(err.Error(), "reflect"):
|
||||
t.Errorf("%d fail: error message includes reflect internals: %v",
|
||||
i, err)
|
||||
default:
|
||||
t.Logf("%d pass: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type confIssue2 struct{ Main struct{ Foo string } }
|
||||
|
||||
var testsIssue2 = []readtest{
|
||||
{"[main]\n;\nfoo = bar\n", &confIssue2{struct{ Foo string }{"bar"}}, true},
|
||||
{"[main]\r\n;\r\nfoo = bar\r\n", &confIssue2{struct{ Foo string }{"bar"}}, true},
|
||||
}
|
||||
|
||||
func TestIssue2(t *testing.T) {
|
||||
for i, tt := range testsIssue2 {
|
||||
id := fmt.Sprintf("issue2:%d", i)
|
||||
testRead(t, id, tt)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
package gcfg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
import (
|
||||
"github.com/scalingdata/gcfg/scanner"
|
||||
"github.com/scalingdata/gcfg/token"
|
||||
)
|
||||
|
||||
var unescape = map[rune]rune{'\\': '\\', '"': '"', 'n': '\n', 't': '\t'}
|
||||
|
||||
// no error: invalid literals should be caught by scanner
|
||||
func unquote(s string) string {
|
||||
u, q, esc := make([]rune, 0, len(s)), false, false
|
||||
for _, c := range s {
|
||||
if esc {
|
||||
uc, ok := unescape[c]
|
||||
switch {
|
||||
case ok:
|
||||
u = append(u, uc)
|
||||
fallthrough
|
||||
case !q && c == '\n':
|
||||
esc = false
|
||||
continue
|
||||
}
|
||||
panic("invalid escape sequence")
|
||||
}
|
||||
switch c {
|
||||
case '"':
|
||||
q = !q
|
||||
case '\\':
|
||||
esc = true
|
||||
default:
|
||||
u = append(u, c)
|
||||
}
|
||||
}
|
||||
if q {
|
||||
panic("missing end quote")
|
||||
}
|
||||
if esc {
|
||||
panic("invalid escape sequence")
|
||||
}
|
||||
return string(u)
|
||||
}
|
||||
|
||||
func readInto(config interface{}, fset *token.FileSet, file *token.File, src []byte) error {
|
||||
var s scanner.Scanner
|
||||
var errs scanner.ErrorList
|
||||
s.Init(file, src, func(p token.Position, m string) { errs.Add(p, m) }, 0)
|
||||
sect, sectsub := "", ""
|
||||
pos, tok, lit := s.Scan()
|
||||
errfn := func(msg string) error {
|
||||
return fmt.Errorf("%s: %s", fset.Position(pos), msg)
|
||||
}
|
||||
for {
|
||||
if errs.Len() > 0 {
|
||||
return errs.Err()
|
||||
}
|
||||
switch tok {
|
||||
case token.EOF:
|
||||
return nil
|
||||
case token.EOL, token.COMMENT:
|
||||
pos, tok, lit = s.Scan()
|
||||
case token.LBRACK:
|
||||
pos, tok, lit = s.Scan()
|
||||
if errs.Len() > 0 {
|
||||
return errs.Err()
|
||||
}
|
||||
if tok != token.IDENT {
|
||||
return errfn("expected section name")
|
||||
}
|
||||
sect, sectsub = lit, ""
|
||||
pos, tok, lit = s.Scan()
|
||||
if errs.Len() > 0 {
|
||||
return errs.Err()
|
||||
}
|
||||
if tok == token.STRING {
|
||||
sectsub = unquote(lit)
|
||||
if sectsub == "" {
|
||||
return errfn("empty subsection name")
|
||||
}
|
||||
pos, tok, lit = s.Scan()
|
||||
if errs.Len() > 0 {
|
||||
return errs.Err()
|
||||
}
|
||||
}
|
||||
if tok != token.RBRACK {
|
||||
if sectsub == "" {
|
||||
return errfn("expected subsection name or right bracket")
|
||||
}
|
||||
return errfn("expected right bracket")
|
||||
}
|
||||
pos, tok, lit = s.Scan()
|
||||
if tok != token.EOL && tok != token.EOF && tok != token.COMMENT {
|
||||
return errfn("expected EOL, EOF, or comment")
|
||||
}
|
||||
case token.IDENT:
|
||||
if sect == "" {
|
||||
return errfn("expected section header")
|
||||
}
|
||||
n := lit
|
||||
pos, tok, lit = s.Scan()
|
||||
if errs.Len() > 0 {
|
||||
return errs.Err()
|
||||
}
|
||||
blank, v := tok == token.EOF || tok == token.EOL || tok == token.COMMENT, ""
|
||||
if !blank {
|
||||
if tok != token.ASSIGN {
|
||||
return errfn("expected '='")
|
||||
}
|
||||
pos, tok, lit = s.Scan()
|
||||
if errs.Len() > 0 {
|
||||
return errs.Err()
|
||||
}
|
||||
if tok != token.STRING {
|
||||
return errfn("expected value")
|
||||
}
|
||||
v = unquote(lit)
|
||||
pos, tok, lit = s.Scan()
|
||||
if errs.Len() > 0 {
|
||||
return errs.Err()
|
||||
}
|
||||
if tok != token.EOL && tok != token.EOF && tok != token.COMMENT {
|
||||
return errfn("expected EOL, EOF, or comment")
|
||||
}
|
||||
}
|
||||
err := set(config, sect, sectsub, n, blank, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if sect == "" {
|
||||
return errfn("expected section header")
|
||||
}
|
||||
return errfn("expected section header or variable declaration")
|
||||
}
|
||||
}
|
||||
panic("never reached")
|
||||
}
|
||||
|
||||
// ReadInto reads gcfg formatted data from reader and sets the values into the
|
||||
// corresponding fields in config.
|
||||
func ReadInto(config interface{}, reader io.Reader) error {
|
||||
src, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fset := token.NewFileSet()
|
||||
file := fset.AddFile("", fset.Base(), len(src))
|
||||
return readInto(config, fset, file, src)
|
||||
}
|
||||
|
||||
// ReadStringInto reads gcfg formatted data from str and sets the values into
|
||||
// the corresponding fields in config.
|
||||
func ReadStringInto(config interface{}, str string) error {
|
||||
r := strings.NewReader(str)
|
||||
return ReadInto(config, r)
|
||||
}
|
||||
|
||||
// ReadFileInto reads gcfg formatted data from the file filename and sets the
|
||||
// values into the corresponding fields in config.
|
||||
func ReadFileInto(config interface{}, filename string) error {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
src, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fset := token.NewFileSet()
|
||||
file := fset.AddFile(filename, fset.Base(), len(src))
|
||||
return readInto(config, fset, file, src)
|
||||
}
|
|
@ -0,0 +1,333 @@
|
|||
package gcfg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
// 64 spaces
|
||||
sp64 = " "
|
||||
// 512 spaces
|
||||
sp512 = sp64 + sp64 + sp64 + sp64 + sp64 + sp64 + sp64 + sp64
|
||||
// 4096 spaces
|
||||
sp4096 = sp512 + sp512 + sp512 + sp512 + sp512 + sp512 + sp512 + sp512
|
||||
)
|
||||
|
||||
type cBasic struct {
|
||||
Section cBasicS1
|
||||
Hyphen_In_Section cBasicS2
|
||||
unexported cBasicS1
|
||||
Exported cBasicS3
|
||||
TagName cBasicS1 `gcfg:"tag-name"`
|
||||
}
|
||||
type cBasicS1 struct {
|
||||
Name string
|
||||
Int int
|
||||
PName *string
|
||||
}
|
||||
type cBasicS2 struct {
|
||||
Hyphen_In_Name string
|
||||
}
|
||||
type cBasicS3 struct {
|
||||
unexported string
|
||||
}
|
||||
|
||||
type nonMulti []string
|
||||
|
||||
type unmarshalable string
|
||||
|
||||
func (u *unmarshalable) UnmarshalText(text []byte) error {
|
||||
s := string(text)
|
||||
if s == "error" {
|
||||
return fmt.Errorf("%s", s)
|
||||
}
|
||||
*u = unmarshalable(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ textUnmarshaler = new(unmarshalable)
|
||||
|
||||
type cUni struct {
|
||||
X甲 cUniS1
|
||||
XSection cUniS2
|
||||
}
|
||||
type cUniS1 struct {
|
||||
X乙 string
|
||||
}
|
||||
type cUniS2 struct {
|
||||
XName string
|
||||
}
|
||||
|
||||
type cMulti struct {
|
||||
M1 cMultiS1
|
||||
M2 cMultiS2
|
||||
M3 cMultiS3
|
||||
}
|
||||
type cMultiS1 struct{ Multi []string }
|
||||
type cMultiS2 struct{ NonMulti nonMulti }
|
||||
type cMultiS3 struct{ MultiInt []int }
|
||||
|
||||
type cSubs struct{ Sub map[string]*cSubsS1 }
|
||||
type cSubsS1 struct{ Name string }
|
||||
|
||||
type cBool struct{ Section cBoolS1 }
|
||||
type cBoolS1 struct{ Bool bool }
|
||||
|
||||
type cTxUnm struct{ Section cTxUnmS1 }
|
||||
type cTxUnmS1 struct{ Name unmarshalable }
|
||||
|
||||
type cNum struct {
|
||||
N1 cNumS1
|
||||
N2 cNumS2
|
||||
N3 cNumS3
|
||||
}
|
||||
type cNumS1 struct {
|
||||
Int int
|
||||
IntDHO int `gcfg:",int=dho"`
|
||||
Big *big.Int
|
||||
}
|
||||
type cNumS2 struct {
|
||||
MultiInt []int
|
||||
MultiBig []*big.Int
|
||||
}
|
||||
type cNumS3 struct{ FileMode os.FileMode }
|
||||
type readtest struct {
|
||||
gcfg string
|
||||
exp interface{}
|
||||
ok bool
|
||||
}
|
||||
|
||||
func newString(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
var readtests = []struct {
|
||||
group string
|
||||
tests []readtest
|
||||
}{{"scanning", []readtest{
|
||||
{"[section]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
// hyphen in name
|
||||
{"[hyphen-in-section]\nhyphen-in-name=value", &cBasic{Hyphen_In_Section: cBasicS2{Hyphen_In_Name: "value"}}, true},
|
||||
// quoted string value
|
||||
{"[section]\nname=\"\"", &cBasic{Section: cBasicS1{Name: ""}}, true},
|
||||
{"[section]\nname=\" \"", &cBasic{Section: cBasicS1{Name: " "}}, true},
|
||||
{"[section]\nname=\"value\"", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"[section]\nname=\" value \"", &cBasic{Section: cBasicS1{Name: " value "}}, true},
|
||||
{"\n[section]\nname=\"va ; lue\"", &cBasic{Section: cBasicS1{Name: "va ; lue"}}, true},
|
||||
{"[section]\nname=\"val\" \"ue\"", &cBasic{Section: cBasicS1{Name: "val ue"}}, true},
|
||||
{"[section]\nname=\"value", &cBasic{}, false},
|
||||
// escape sequences
|
||||
{"[section]\nname=\"va\\\\lue\"", &cBasic{Section: cBasicS1{Name: "va\\lue"}}, true},
|
||||
{"[section]\nname=\"va\\\"lue\"", &cBasic{Section: cBasicS1{Name: "va\"lue"}}, true},
|
||||
{"[section]\nname=\"va\\nlue\"", &cBasic{Section: cBasicS1{Name: "va\nlue"}}, true},
|
||||
{"[section]\nname=\"va\\tlue\"", &cBasic{Section: cBasicS1{Name: "va\tlue"}}, true},
|
||||
{"\n[section]\nname=\\", &cBasic{}, false},
|
||||
{"\n[section]\nname=\\a", &cBasic{}, false},
|
||||
{"\n[section]\nname=\"val\\a\"", &cBasic{}, false},
|
||||
{"\n[section]\nname=val\\", &cBasic{}, false},
|
||||
{"\n[sub \"A\\\n\"]\nname=value", &cSubs{}, false},
|
||||
{"\n[sub \"A\\\t\"]\nname=value", &cSubs{}, false},
|
||||
// broken line
|
||||
{"[section]\nname=value \\\n value", &cBasic{Section: cBasicS1{Name: "value value"}}, true},
|
||||
{"[section]\nname=\"value \\\n value\"", &cBasic{}, false},
|
||||
}}, {"scanning:whitespace", []readtest{
|
||||
{" \n[section]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{" [section]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"\t[section]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"[ section]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"[section ]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"[section]\n name=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"[section]\nname =value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"[section]\nname= value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"[section]\nname=value ", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"[section]\r\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"[section]\r\nname=value\r\n", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{";cmnt\r\n[section]\r\nname=value\r\n", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
// long lines
|
||||
{sp4096 + "[section]\nname=value\n", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"[" + sp4096 + "section]\nname=value\n", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"[section" + sp4096 + "]\nname=value\n", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"[section]" + sp4096 + "\nname=value\n", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"[section]\n" + sp4096 + "name=value\n", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"[section]\nname" + sp4096 + "=value\n", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"[section]\nname=" + sp4096 + "value\n", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"[section]\nname=value\n" + sp4096, &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
}}, {"scanning:comments", []readtest{
|
||||
{"; cmnt\n[section]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"# cmnt\n[section]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{" ; cmnt\n[section]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"\t; cmnt\n[section]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"\n[section]; cmnt\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"\n[section] ; cmnt\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"\n[section]\nname=value; cmnt", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"\n[section]\nname=value ; cmnt", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"\n[section]\nname=\"value\" ; cmnt", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"\n[section]\nname=value ; \"cmnt", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"\n[section]\nname=\"va ; lue\" ; cmnt", &cBasic{Section: cBasicS1{Name: "va ; lue"}}, true},
|
||||
{"\n[section]\nname=; cmnt", &cBasic{Section: cBasicS1{Name: ""}}, true},
|
||||
}}, {"scanning:subsections", []readtest{
|
||||
{"\n[sub \"A\"]\nname=value", &cSubs{map[string]*cSubsS1{"A": &cSubsS1{"value"}}}, true},
|
||||
{"\n[sub \"b\"]\nname=value", &cSubs{map[string]*cSubsS1{"b": &cSubsS1{"value"}}}, true},
|
||||
{"\n[sub \"A\\\\\"]\nname=value", &cSubs{map[string]*cSubsS1{"A\\": &cSubsS1{"value"}}}, true},
|
||||
{"\n[sub \"A\\\"\"]\nname=value", &cSubs{map[string]*cSubsS1{"A\"": &cSubsS1{"value"}}}, true},
|
||||
}}, {"syntax", []readtest{
|
||||
// invalid line
|
||||
{"\n[section]\n=", &cBasic{}, false},
|
||||
// no section
|
||||
{"name=value", &cBasic{}, false},
|
||||
// empty section
|
||||
{"\n[]\nname=value", &cBasic{}, false},
|
||||
// empty subsection
|
||||
{"\n[sub \"\"]\nname=value", &cSubs{}, false},
|
||||
}}, {"setting", []readtest{
|
||||
{"[section]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
// pointer
|
||||
{"[section]", &cBasic{Section: cBasicS1{PName: nil}}, true},
|
||||
{"[section]\npname=value", &cBasic{Section: cBasicS1{PName: newString("value")}}, true},
|
||||
// section name not matched
|
||||
{"\n[nonexistent]\nname=value", &cBasic{}, false},
|
||||
// subsection name not matched
|
||||
{"\n[section \"nonexistent\"]\nname=value", &cBasic{}, false},
|
||||
// variable name not matched
|
||||
{"\n[section]\nnonexistent=value", &cBasic{}, false},
|
||||
// hyphen in name
|
||||
{"[hyphen-in-section]\nhyphen-in-name=value", &cBasic{Hyphen_In_Section: cBasicS2{Hyphen_In_Name: "value"}}, true},
|
||||
// ignore unexported fields
|
||||
{"[unexported]\nname=value", &cBasic{}, false},
|
||||
{"[exported]\nunexported=value", &cBasic{}, false},
|
||||
// 'X' prefix for non-upper/lower-case letters
|
||||
{"[甲]\n乙=丙", &cUni{X甲: cUniS1{X乙: "丙"}}, true},
|
||||
//{"[section]\nxname=value", &cBasic{XSection: cBasicS4{XName: "value"}}, false},
|
||||
//{"[xsection]\nname=value", &cBasic{XSection: cBasicS4{XName: "value"}}, false},
|
||||
// name specified as struct tag
|
||||
{"[tag-name]\nname=value", &cBasic{TagName: cBasicS1{Name: "value"}}, true},
|
||||
}}, {"multivalue", []readtest{
|
||||
// unnamed slice type: treat as multi-value
|
||||
{"\n[m1]", &cMulti{M1: cMultiS1{}}, true},
|
||||
{"\n[m1]\nmulti=value", &cMulti{M1: cMultiS1{[]string{"value"}}}, true},
|
||||
{"\n[m1]\nmulti=value1\nmulti=value2", &cMulti{M1: cMultiS1{[]string{"value1", "value2"}}}, true},
|
||||
// "blank" empties multi-valued slice -- here same result as above
|
||||
{"\n[m1]\nmulti\nmulti=value1\nmulti=value2", &cMulti{M1: cMultiS1{[]string{"value1", "value2"}}}, true},
|
||||
// named slice type: do not treat as multi-value
|
||||
{"\n[m2]", &cMulti{}, true},
|
||||
{"\n[m2]\nmulti=value", &cMulti{}, false},
|
||||
{"\n[m2]\nmulti=value1\nmulti=value2", &cMulti{}, false},
|
||||
}}, {"type:string", []readtest{
|
||||
{"[section]\nname=value", &cBasic{Section: cBasicS1{Name: "value"}}, true},
|
||||
{"[section]\nname=", &cBasic{Section: cBasicS1{Name: ""}}, true},
|
||||
}}, {"type:bool", []readtest{
|
||||
// explicit values
|
||||
{"[section]\nbool=true", &cBool{cBoolS1{true}}, true},
|
||||
{"[section]\nbool=yes", &cBool{cBoolS1{true}}, true},
|
||||
{"[section]\nbool=on", &cBool{cBoolS1{true}}, true},
|
||||
{"[section]\nbool=1", &cBool{cBoolS1{true}}, true},
|
||||
{"[section]\nbool=tRuE", &cBool{cBoolS1{true}}, true},
|
||||
{"[section]\nbool=false", &cBool{cBoolS1{false}}, true},
|
||||
{"[section]\nbool=no", &cBool{cBoolS1{false}}, true},
|
||||
{"[section]\nbool=off", &cBool{cBoolS1{false}}, true},
|
||||
{"[section]\nbool=0", &cBool{cBoolS1{false}}, true},
|
||||
{"[section]\nbool=NO", &cBool{cBoolS1{false}}, true},
|
||||
// "blank" value handled as true
|
||||
{"[section]\nbool", &cBool{cBoolS1{true}}, true},
|
||||
// bool parse errors
|
||||
{"[section]\nbool=maybe", &cBool{}, false},
|
||||
{"[section]\nbool=t", &cBool{}, false},
|
||||
{"[section]\nbool=truer", &cBool{}, false},
|
||||
{"[section]\nbool=2", &cBool{}, false},
|
||||
{"[section]\nbool=-1", &cBool{}, false},
|
||||
}}, {"type:numeric", []readtest{
|
||||
{"[section]\nint=0", &cBasic{Section: cBasicS1{Int: 0}}, true},
|
||||
{"[section]\nint=1", &cBasic{Section: cBasicS1{Int: 1}}, true},
|
||||
{"[section]\nint=-1", &cBasic{Section: cBasicS1{Int: -1}}, true},
|
||||
{"[section]\nint=0.2", &cBasic{}, false},
|
||||
{"[section]\nint=1e3", &cBasic{}, false},
|
||||
// primitive [u]int(|8|16|32|64) and big.Int is parsed as dec or hex (not octal)
|
||||
{"[n1]\nint=010", &cNum{N1: cNumS1{Int: 10}}, true},
|
||||
{"[n1]\nint=0x10", &cNum{N1: cNumS1{Int: 0x10}}, true},
|
||||
{"[n1]\nbig=1", &cNum{N1: cNumS1{Big: big.NewInt(1)}}, true},
|
||||
{"[n1]\nbig=0x10", &cNum{N1: cNumS1{Big: big.NewInt(0x10)}}, true},
|
||||
{"[n1]\nbig=010", &cNum{N1: cNumS1{Big: big.NewInt(10)}}, true},
|
||||
{"[n2]\nmultiint=010", &cNum{N2: cNumS2{MultiInt: []int{10}}}, true},
|
||||
{"[n2]\nmultibig=010", &cNum{N2: cNumS2{MultiBig: []*big.Int{big.NewInt(10)}}}, true},
|
||||
// set parse mode for int types via struct tag
|
||||
{"[n1]\nintdho=010", &cNum{N1: cNumS1{IntDHO: 010}}, true},
|
||||
// octal allowed for named type
|
||||
{"[n3]\nfilemode=0777", &cNum{N3: cNumS3{FileMode: 0777}}, true},
|
||||
}}, {"type:textUnmarshaler", []readtest{
|
||||
{"[section]\nname=value", &cTxUnm{Section: cTxUnmS1{Name: "value"}}, true},
|
||||
{"[section]\nname=error", &cTxUnm{}, false},
|
||||
}},
|
||||
}
|
||||
|
||||
func TestReadStringInto(t *testing.T) {
|
||||
for _, tg := range readtests {
|
||||
for i, tt := range tg.tests {
|
||||
id := fmt.Sprintf("%s:%d", tg.group, i)
|
||||
testRead(t, id, tt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadStringIntoMultiBlankPreset(t *testing.T) {
|
||||
tt := readtest{"\n[m1]\nmulti\nmulti=value1\nmulti=value2", &cMulti{M1: cMultiS1{[]string{"value1", "value2"}}}, true}
|
||||
cfg := &cMulti{M1: cMultiS1{[]string{"preset1", "preset2"}}}
|
||||
testReadInto(t, "multi:blank", tt, cfg)
|
||||
}
|
||||
|
||||
func testRead(t *testing.T, id string, tt readtest) {
|
||||
// get the type of the expected result
|
||||
restyp := reflect.TypeOf(tt.exp).Elem()
|
||||
// create a new instance to hold the actual result
|
||||
res := reflect.New(restyp).Interface()
|
||||
testReadInto(t, id, tt, res)
|
||||
}
|
||||
|
||||
func testReadInto(t *testing.T, id string, tt readtest, res interface{}) {
|
||||
err := ReadStringInto(res, tt.gcfg)
|
||||
if tt.ok {
|
||||
if err != nil {
|
||||
t.Errorf("%s fail: got error %v, wanted ok", id, err)
|
||||
return
|
||||
} else if !reflect.DeepEqual(res, tt.exp) {
|
||||
t.Errorf("%s fail: got value %#v, wanted value %#v", id, res, tt.exp)
|
||||
return
|
||||
}
|
||||
if !testing.Short() {
|
||||
t.Logf("%s pass: got value %#v", id, res)
|
||||
}
|
||||
} else { // !tt.ok
|
||||
if err == nil {
|
||||
t.Errorf("%s fail: got value %#v, wanted error", id, res)
|
||||
return
|
||||
}
|
||||
if !testing.Short() {
|
||||
t.Logf("%s pass: got error %v", id, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFileInto(t *testing.T) {
|
||||
res := &struct{ Section struct{ Name string } }{}
|
||||
err := ReadFileInto(res, "testdata/gcfg_test.gcfg")
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
if "value" != res.Section.Name {
|
||||
t.Errorf("got %q, wanted %q", res.Section.Name, "value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFileIntoUnicode(t *testing.T) {
|
||||
res := &struct{ X甲 struct{ X乙 string } }{}
|
||||
err := ReadFileInto(res, "testdata/gcfg_unicode_test.gcfg")
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
if "丙" != res.X甲.X乙 {
|
||||
t.Errorf("got %q, wanted %q", res.X甲.X乙, "丙")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
)
|
||||
|
||||
import (
|
||||
"github.com/scalingdata/gcfg/token"
|
||||
)
|
||||
|
||||
// In an ErrorList, an error is represented by an *Error.
|
||||
// The position Pos, if valid, points to the beginning of
|
||||
// the offending token, and the error condition is described
|
||||
// by Msg.
|
||||
//
|
||||
type Error struct {
|
||||
Pos token.Position
|
||||
Msg string
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e Error) Error() string {
|
||||
if e.Pos.Filename != "" || e.Pos.IsValid() {
|
||||
// don't print "<unknown position>"
|
||||
// TODO(gri) reconsider the semantics of Position.IsValid
|
||||
return e.Pos.String() + ": " + e.Msg
|
||||
}
|
||||
return e.Msg
|
||||
}
|
||||
|
||||
// ErrorList is a list of *Errors.
|
||||
// The zero value for an ErrorList is an empty ErrorList ready to use.
|
||||
//
|
||||
type ErrorList []*Error
|
||||
|
||||
// Add adds an Error with given position and error message to an ErrorList.
|
||||
func (p *ErrorList) Add(pos token.Position, msg string) {
|
||||
*p = append(*p, &Error{pos, msg})
|
||||
}
|
||||
|
||||
// Reset resets an ErrorList to no errors.
|
||||
func (p *ErrorList) Reset() { *p = (*p)[0:0] }
|
||||
|
||||
// ErrorList implements the sort Interface.
|
||||
func (p ErrorList) Len() int { return len(p) }
|
||||
func (p ErrorList) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
|
||||
func (p ErrorList) Less(i, j int) bool {
|
||||
e := &p[i].Pos
|
||||
f := &p[j].Pos
|
||||
if e.Filename < f.Filename {
|
||||
return true
|
||||
}
|
||||
if e.Filename == f.Filename {
|
||||
return e.Offset < f.Offset
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Sort sorts an ErrorList. *Error entries are sorted by position,
|
||||
// other errors are sorted by error message, and before any *Error
|
||||
// entry.
|
||||
//
|
||||
func (p ErrorList) Sort() {
|
||||
sort.Sort(p)
|
||||
}
|
||||
|
||||
// RemoveMultiples sorts an ErrorList and removes all but the first error per line.
|
||||
func (p *ErrorList) RemoveMultiples() {
|
||||
sort.Sort(p)
|
||||
var last token.Position // initial last.Line is != any legal error line
|
||||
i := 0
|
||||
for _, e := range *p {
|
||||
if e.Pos.Filename != last.Filename || e.Pos.Line != last.Line {
|
||||
last = e.Pos
|
||||
(*p)[i] = e
|
||||
i++
|
||||
}
|
||||
}
|
||||
(*p) = (*p)[0:i]
|
||||
}
|
||||
|
||||
// An ErrorList implements the error interface.
|
||||
func (p ErrorList) Error() string {
|
||||
switch len(p) {
|
||||
case 0:
|
||||
return "no errors"
|
||||
case 1:
|
||||
return p[0].Error()
|
||||
}
|
||||
return fmt.Sprintf("%s (and %d more errors)", p[0], len(p)-1)
|
||||
}
|
||||
|
||||
// Err returns an error equivalent to this error list.
|
||||
// If the list is empty, Err returns nil.
|
||||
func (p ErrorList) Err() error {
|
||||
if len(p) == 0 {
|
||||
return nil
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// PrintError is a utility function that prints a list of errors to w,
|
||||
// one error per line, if the err parameter is an ErrorList. Otherwise
|
||||
// it prints the err string.
|
||||
//
|
||||
func PrintError(w io.Writer, err error) {
|
||||
if list, ok := err.(ErrorList); ok {
|
||||
for _, e := range list {
|
||||
fmt.Fprintf(w, "%s\n", e)
|
||||
}
|
||||
} else if err != nil {
|
||||
fmt.Fprintf(w, "%s\n", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package scanner_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
import (
|
||||
"github.com/scalingdata/gcfg/scanner"
|
||||
"github.com/scalingdata/gcfg/token"
|
||||
)
|
||||
|
||||
func ExampleScanner_Scan() {
|
||||
// src is the input that we want to tokenize.
|
||||
src := []byte(`[profile "A"]
|
||||
color = blue ; Comment`)
|
||||
|
||||
// Initialize the scanner.
|
||||
var s scanner.Scanner
|
||||
fset := token.NewFileSet() // positions are relative to fset
|
||||
file := fset.AddFile("", fset.Base(), len(src)) // register input "file"
|
||||
s.Init(file, src, nil /* no error handler */, scanner.ScanComments)
|
||||
|
||||
// Repeated calls to Scan yield the token sequence found in the input.
|
||||
for {
|
||||
pos, tok, lit := s.Scan()
|
||||
if tok == token.EOF {
|
||||
break
|
||||
}
|
||||
fmt.Printf("%s\t%q\t%q\n", fset.Position(pos), tok, lit)
|
||||
}
|
||||
|
||||
// output:
|
||||
// 1:1 "[" ""
|
||||
// 1:2 "IDENT" "profile"
|
||||
// 1:10 "STRING" "\"A\""
|
||||
// 1:13 "]" ""
|
||||
// 1:14 "\n" ""
|
||||
// 2:1 "IDENT" "color"
|
||||
// 2:7 "=" ""
|
||||
// 2:9 "STRING" "blue"
|
||||
// 2:14 "COMMENT" "; Comment"
|
||||
}
|
|
@ -0,0 +1,342 @@
|
|||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package scanner implements a scanner for gcfg configuration text.
|
||||
// It takes a []byte as source which can then be tokenized
|
||||
// through repeated calls to the Scan method.
|
||||
//
|
||||
// Note that the API for the scanner package may change to accommodate new
|
||||
// features or implementation changes in gcfg.
|
||||
//
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
import (
|
||||
"github.com/scalingdata/gcfg/token"
|
||||
)
|
||||
|
||||
// An ErrorHandler may be provided to Scanner.Init. If a syntax error is
|
||||
// encountered and a handler was installed, the handler is called with a
|
||||
// position and an error message. The position points to the beginning of
|
||||
// the offending token.
|
||||
//
|
||||
type ErrorHandler func(pos token.Position, msg string)
|
||||
|
||||
// A Scanner holds the scanner's internal state while processing
|
||||
// a given text. It can be allocated as part of another data
|
||||
// structure but must be initialized via Init before use.
|
||||
//
|
||||
type Scanner struct {
|
||||
// immutable state
|
||||
file *token.File // source file handle
|
||||
dir string // directory portion of file.Name()
|
||||
src []byte // source
|
||||
err ErrorHandler // error reporting; or nil
|
||||
mode Mode // scanning mode
|
||||
|
||||
// scanning state
|
||||
ch rune // current character
|
||||
offset int // character offset
|
||||
rdOffset int // reading offset (position after current character)
|
||||
lineOffset int // current line offset
|
||||
nextVal bool // next token is expected to be a value
|
||||
|
||||
// public state - ok to modify
|
||||
ErrorCount int // number of errors encountered
|
||||
}
|
||||
|
||||
// Read the next Unicode char into s.ch.
|
||||
// s.ch < 0 means end-of-file.
|
||||
//
|
||||
func (s *Scanner) next() {
|
||||
if s.rdOffset < len(s.src) {
|
||||
s.offset = s.rdOffset
|
||||
if s.ch == '\n' {
|
||||
s.lineOffset = s.offset
|
||||
s.file.AddLine(s.offset)
|
||||
}
|
||||
r, w := rune(s.src[s.rdOffset]), 1
|
||||
switch {
|
||||
case r == 0:
|
||||
s.error(s.offset, "illegal character NUL")
|
||||
case r >= 0x80:
|
||||
// not ASCII
|
||||
r, w = utf8.DecodeRune(s.src[s.rdOffset:])
|
||||
if r == utf8.RuneError && w == 1 {
|
||||
s.error(s.offset, "illegal UTF-8 encoding")
|
||||
}
|
||||
}
|
||||
s.rdOffset += w
|
||||
s.ch = r
|
||||
} else {
|
||||
s.offset = len(s.src)
|
||||
if s.ch == '\n' {
|
||||
s.lineOffset = s.offset
|
||||
s.file.AddLine(s.offset)
|
||||
}
|
||||
s.ch = -1 // eof
|
||||
}
|
||||
}
|
||||
|
||||
// A mode value is a set of flags (or 0).
|
||||
// They control scanner behavior.
|
||||
//
|
||||
type Mode uint
|
||||
|
||||
const (
|
||||
ScanComments Mode = 1 << iota // return comments as COMMENT tokens
|
||||
)
|
||||
|
||||
// Init prepares the scanner s to tokenize the text src by setting the
|
||||
// scanner at the beginning of src. The scanner uses the file set file
|
||||
// for position information and it adds line information for each line.
|
||||
// It is ok to re-use the same file when re-scanning the same file as
|
||||
// line information which is already present is ignored. Init causes a
|
||||
// panic if the file size does not match the src size.
|
||||
//
|
||||
// Calls to Scan will invoke the error handler err if they encounter a
|
||||
// syntax error and err is not nil. Also, for each error encountered,
|
||||
// the Scanner field ErrorCount is incremented by one. The mode parameter
|
||||
// determines how comments are handled.
|
||||
//
|
||||
// Note that Init may call err if there is an error in the first character
|
||||
// of the file.
|
||||
//
|
||||
func (s *Scanner) Init(file *token.File, src []byte, err ErrorHandler, mode Mode) {
|
||||
// Explicitly initialize all fields since a scanner may be reused.
|
||||
if file.Size() != len(src) {
|
||||
panic(fmt.Sprintf("file size (%d) does not match src len (%d)", file.Size(), len(src)))
|
||||
}
|
||||
s.file = file
|
||||
s.dir, _ = filepath.Split(file.Name())
|
||||
s.src = src
|
||||
s.err = err
|
||||
s.mode = mode
|
||||
|
||||
s.ch = ' '
|
||||
s.offset = 0
|
||||
s.rdOffset = 0
|
||||
s.lineOffset = 0
|
||||
s.ErrorCount = 0
|
||||
s.nextVal = false
|
||||
|
||||
s.next()
|
||||
}
|
||||
|
||||
func (s *Scanner) error(offs int, msg string) {
|
||||
if s.err != nil {
|
||||
s.err(s.file.Position(s.file.Pos(offs)), msg)
|
||||
}
|
||||
s.ErrorCount++
|
||||
}
|
||||
|
||||
func (s *Scanner) scanComment() string {
|
||||
// initial [;#] already consumed
|
||||
offs := s.offset - 1 // position of initial [;#]
|
||||
|
||||
for s.ch != '\n' && s.ch >= 0 {
|
||||
s.next()
|
||||
}
|
||||
return string(s.src[offs:s.offset])
|
||||
}
|
||||
|
||||
func isLetter(ch rune) bool {
|
||||
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch >= 0x80 && unicode.IsLetter(ch)
|
||||
}
|
||||
|
||||
func isDigit(ch rune) bool {
|
||||
return '0' <= ch && ch <= '9' || ch >= 0x80 && unicode.IsDigit(ch)
|
||||
}
|
||||
|
||||
func (s *Scanner) scanIdentifier() string {
|
||||
offs := s.offset
|
||||
for isLetter(s.ch) || isDigit(s.ch) || s.ch == '-' {
|
||||
s.next()
|
||||
}
|
||||
return string(s.src[offs:s.offset])
|
||||
}
|
||||
|
||||
func (s *Scanner) scanEscape(val bool) {
|
||||
offs := s.offset
|
||||
ch := s.ch
|
||||
s.next() // always make progress
|
||||
switch ch {
|
||||
case '\\', '"':
|
||||
// ok
|
||||
case 'n', 't':
|
||||
if val {
|
||||
break // ok
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
s.error(offs, "unknown escape sequence")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scanner) scanString() string {
|
||||
// '"' opening already consumed
|
||||
offs := s.offset - 1
|
||||
|
||||
for s.ch != '"' {
|
||||
ch := s.ch
|
||||
s.next()
|
||||
if ch == '\n' || ch < 0 {
|
||||
s.error(offs, "string not terminated")
|
||||
break
|
||||
}
|
||||
if ch == '\\' {
|
||||
s.scanEscape(false)
|
||||
}
|
||||
}
|
||||
|
||||
s.next()
|
||||
|
||||
return string(s.src[offs:s.offset])
|
||||
}
|
||||
|
||||
func stripCR(b []byte) []byte {
|
||||
c := make([]byte, len(b))
|
||||
i := 0
|
||||
for _, ch := range b {
|
||||
if ch != '\r' {
|
||||
c[i] = ch
|
||||
i++
|
||||
}
|
||||
}
|
||||
return c[:i]
|
||||
}
|
||||
|
||||
func (s *Scanner) scanValString() string {
|
||||
offs := s.offset
|
||||
|
||||
hasCR := false
|
||||
end := offs
|
||||
inQuote := false
|
||||
loop:
|
||||
for inQuote || s.ch >= 0 && s.ch != '\n' && s.ch != ';' && s.ch != '#' {
|
||||
ch := s.ch
|
||||
s.next()
|
||||
switch {
|
||||
case inQuote && ch == '\\':
|
||||
s.scanEscape(true)
|
||||
case !inQuote && ch == '\\':
|
||||
if s.ch == '\r' {
|
||||
hasCR = true
|
||||
s.next()
|
||||
}
|
||||
if s.ch != '\n' {
|
||||
s.error(offs, "unquoted '\\' must be followed by new line")
|
||||
break loop
|
||||
}
|
||||
s.next()
|
||||
case ch == '"':
|
||||
inQuote = !inQuote
|
||||
case ch == '\r':
|
||||
hasCR = true
|
||||
case ch < 0 || inQuote && ch == '\n':
|
||||
s.error(offs, "string not terminated")
|
||||
break loop
|
||||
}
|
||||
if inQuote || !isWhiteSpace(ch) {
|
||||
end = s.offset
|
||||
}
|
||||
}
|
||||
|
||||
lit := s.src[offs:end]
|
||||
if hasCR {
|
||||
lit = stripCR(lit)
|
||||
}
|
||||
|
||||
return string(lit)
|
||||
}
|
||||
|
||||
func isWhiteSpace(ch rune) bool {
|
||||
return ch == ' ' || ch == '\t' || ch == '\r'
|
||||
}
|
||||
|
||||
func (s *Scanner) skipWhitespace() {
|
||||
for isWhiteSpace(s.ch) {
|
||||
s.next()
|
||||
}
|
||||
}
|
||||
|
||||
// Scan scans the next token and returns the token position, the token,
|
||||
// and its literal string if applicable. The source end is indicated by
|
||||
// token.EOF.
|
||||
//
|
||||
// If the returned token is a literal (token.IDENT, token.STRING) or
|
||||
// token.COMMENT, the literal string has the corresponding value.
|
||||
//
|
||||
// If the returned token is token.ILLEGAL, the literal string is the
|
||||
// offending character.
|
||||
//
|
||||
// In all other cases, Scan returns an empty literal string.
|
||||
//
|
||||
// For more tolerant parsing, Scan will return a valid token if
|
||||
// possible even if a syntax error was encountered. Thus, even
|
||||
// if the resulting token sequence contains no illegal tokens,
|
||||
// a client may not assume that no error occurred. Instead it
|
||||
// must check the scanner's ErrorCount or the number of calls
|
||||
// of the error handler, if there was one installed.
|
||||
//
|
||||
// Scan adds line information to the file added to the file
|
||||
// set with Init. Token positions are relative to that file
|
||||
// and thus relative to the file set.
|
||||
//
|
||||
func (s *Scanner) Scan() (pos token.Pos, tok token.Token, lit string) {
|
||||
scanAgain:
|
||||
s.skipWhitespace()
|
||||
|
||||
// current token start
|
||||
pos = s.file.Pos(s.offset)
|
||||
|
||||
// determine token value
|
||||
switch ch := s.ch; {
|
||||
case s.nextVal:
|
||||
lit = s.scanValString()
|
||||
tok = token.STRING
|
||||
s.nextVal = false
|
||||
case isLetter(ch):
|
||||
lit = s.scanIdentifier()
|
||||
tok = token.IDENT
|
||||
default:
|
||||
s.next() // always make progress
|
||||
switch ch {
|
||||
case -1:
|
||||
tok = token.EOF
|
||||
case '\n':
|
||||
tok = token.EOL
|
||||
case '"':
|
||||
tok = token.STRING
|
||||
lit = s.scanString()
|
||||
case '[':
|
||||
tok = token.LBRACK
|
||||
case ']':
|
||||
tok = token.RBRACK
|
||||
case ';', '#':
|
||||
// comment
|
||||
lit = s.scanComment()
|
||||
if s.mode&ScanComments == 0 {
|
||||
// skip comment
|
||||
goto scanAgain
|
||||
}
|
||||
tok = token.COMMENT
|
||||
case '=':
|
||||
tok = token.ASSIGN
|
||||
s.nextVal = true
|
||||
default:
|
||||
s.error(s.file.Offset(pos), fmt.Sprintf("illegal character %#U", ch))
|
||||
tok = token.ILLEGAL
|
||||
lit = string(ch)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -0,0 +1,417 @@
|
|||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
import (
|
||||
"github.com/scalingdata/gcfg/token"
|
||||
)
|
||||
|
||||
var fset = token.NewFileSet()
|
||||
|
||||
const /* class */ (
|
||||
special = iota
|
||||
literal
|
||||
operator
|
||||
)
|
||||
|
||||
func tokenclass(tok token.Token) int {
|
||||
switch {
|
||||
case tok.IsLiteral():
|
||||
return literal
|
||||
case tok.IsOperator():
|
||||
return operator
|
||||
}
|
||||
return special
|
||||
}
|
||||
|
||||
type elt struct {
|
||||
tok token.Token
|
||||
lit string
|
||||
class int
|
||||
pre string
|
||||
suf string
|
||||
}
|
||||
|
||||
var tokens = [...]elt{
|
||||
// Special tokens
|
||||
{token.COMMENT, "; a comment", special, "", "\n"},
|
||||
{token.COMMENT, "# a comment", special, "", "\n"},
|
||||
|
||||
// Operators and delimiters
|
||||
{token.ASSIGN, "=", operator, "", "value"},
|
||||
{token.LBRACK, "[", operator, "", ""},
|
||||
{token.RBRACK, "]", operator, "", ""},
|
||||
{token.EOL, "\n", operator, "", ""},
|
||||
|
||||
// Identifiers
|
||||
{token.IDENT, "foobar", literal, "", ""},
|
||||
{token.IDENT, "a۰۱۸", literal, "", ""},
|
||||
{token.IDENT, "foo६४", literal, "", ""},
|
||||
{token.IDENT, "bar9876", literal, "", ""},
|
||||
{token.IDENT, "foo-bar", literal, "", ""},
|
||||
{token.IDENT, "foo", literal, ";\n", ""},
|
||||
// String literals (subsection names)
|
||||
{token.STRING, `"foobar"`, literal, "", ""},
|
||||
{token.STRING, `"\""`, literal, "", ""},
|
||||
// String literals (values)
|
||||
{token.STRING, `"\n"`, literal, "=", ""},
|
||||
{token.STRING, `"foobar"`, literal, "=", ""},
|
||||
{token.STRING, `"foo\nbar"`, literal, "=", ""},
|
||||
{token.STRING, `"foo\"bar"`, literal, "=", ""},
|
||||
{token.STRING, `"foo\\bar"`, literal, "=", ""},
|
||||
{token.STRING, `"foobar"`, literal, "=", ""},
|
||||
{token.STRING, `"foobar"`, literal, "= ", ""},
|
||||
{token.STRING, `"foobar"`, literal, "=", "\n"},
|
||||
{token.STRING, `"foobar"`, literal, "=", ";"},
|
||||
{token.STRING, `"foobar"`, literal, "=", " ;"},
|
||||
{token.STRING, `"foobar"`, literal, "=", "#"},
|
||||
{token.STRING, `"foobar"`, literal, "=", " #"},
|
||||
{token.STRING, "foobar", literal, "=", ""},
|
||||
{token.STRING, "foobar", literal, "= ", ""},
|
||||
{token.STRING, "foobar", literal, "=", " "},
|
||||
{token.STRING, `"foo" "bar"`, literal, "=", " "},
|
||||
{token.STRING, "foo\\\nbar", literal, "=", ""},
|
||||
{token.STRING, "foo\\\r\nbar", literal, "=", ""},
|
||||
}
|
||||
|
||||
const whitespace = " \t \n\n\n" // to separate tokens
|
||||
|
||||
var source = func() []byte {
|
||||
var src []byte
|
||||
for _, t := range tokens {
|
||||
src = append(src, t.pre...)
|
||||
src = append(src, t.lit...)
|
||||
src = append(src, t.suf...)
|
||||
src = append(src, whitespace...)
|
||||
}
|
||||
return src
|
||||
}()
|
||||
|
||||
func newlineCount(s string) int {
|
||||
n := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == '\n' {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func checkPos(t *testing.T, lit string, p token.Pos, expected token.Position) {
|
||||
pos := fset.Position(p)
|
||||
if pos.Filename != expected.Filename {
|
||||
t.Errorf("bad filename for %q: got %s, expected %s", lit, pos.Filename, expected.Filename)
|
||||
}
|
||||
if pos.Offset != expected.Offset {
|
||||
t.Errorf("bad position for %q: got %d, expected %d", lit, pos.Offset, expected.Offset)
|
||||
}
|
||||
if pos.Line != expected.Line {
|
||||
t.Errorf("bad line for %q: got %d, expected %d", lit, pos.Line, expected.Line)
|
||||
}
|
||||
if pos.Column != expected.Column {
|
||||
t.Errorf("bad column for %q: got %d, expected %d", lit, pos.Column, expected.Column)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that calling Scan() provides the correct results.
|
||||
func TestScan(t *testing.T) {
|
||||
// make source
|
||||
src_linecount := newlineCount(string(source))
|
||||
whitespace_linecount := newlineCount(whitespace)
|
||||
|
||||
index := 0
|
||||
|
||||
// error handler
|
||||
eh := func(_ token.Position, msg string) {
|
||||
t.Errorf("%d: error handler called (msg = %s)", index, msg)
|
||||
}
|
||||
|
||||
// verify scan
|
||||
var s Scanner
|
||||
s.Init(fset.AddFile("", fset.Base(), len(source)), source, eh, ScanComments)
|
||||
// epos is the expected position
|
||||
epos := token.Position{
|
||||
Filename: "",
|
||||
Offset: 0,
|
||||
Line: 1,
|
||||
Column: 1,
|
||||
}
|
||||
for {
|
||||
pos, tok, lit := s.Scan()
|
||||
if lit == "" {
|
||||
// no literal value for non-literal tokens
|
||||
lit = tok.String()
|
||||
}
|
||||
e := elt{token.EOF, "", special, "", ""}
|
||||
if index < len(tokens) {
|
||||
e = tokens[index]
|
||||
}
|
||||
if tok == token.EOF {
|
||||
lit = "<EOF>"
|
||||
epos.Line = src_linecount
|
||||
epos.Column = 2
|
||||
}
|
||||
if e.pre != "" && strings.ContainsRune("=;#", rune(e.pre[0])) {
|
||||
epos.Column = 1
|
||||
checkPos(t, lit, pos, epos)
|
||||
var etok token.Token
|
||||
if e.pre[0] == '=' {
|
||||
etok = token.ASSIGN
|
||||
} else {
|
||||
etok = token.COMMENT
|
||||
}
|
||||
if tok != etok {
|
||||
t.Errorf("bad token for %q: got %q, expected %q", lit, tok, etok)
|
||||
}
|
||||
pos, tok, lit = s.Scan()
|
||||
}
|
||||
epos.Offset += len(e.pre)
|
||||
if tok != token.EOF {
|
||||
epos.Column = 1 + len(e.pre)
|
||||
}
|
||||
if e.pre != "" && e.pre[len(e.pre)-1] == '\n' {
|
||||
epos.Offset--
|
||||
epos.Column--
|
||||
checkPos(t, lit, pos, epos)
|
||||
if tok != token.EOL {
|
||||
t.Errorf("bad token for %q: got %q, expected %q", lit, tok, token.EOL)
|
||||
}
|
||||
epos.Line++
|
||||
epos.Offset++
|
||||
epos.Column = 1
|
||||
pos, tok, lit = s.Scan()
|
||||
}
|
||||
checkPos(t, lit, pos, epos)
|
||||
if tok != e.tok {
|
||||
t.Errorf("bad token for %q: got %q, expected %q", lit, tok, e.tok)
|
||||
}
|
||||
if e.tok.IsLiteral() {
|
||||
// no CRs in value string literals
|
||||
elit := e.lit
|
||||
if strings.ContainsRune(e.pre, '=') {
|
||||
elit = string(stripCR([]byte(elit)))
|
||||
epos.Offset += len(e.lit) - len(lit) // correct position
|
||||
}
|
||||
if lit != elit {
|
||||
t.Errorf("bad literal for %q: got %q, expected %q", lit, lit, elit)
|
||||
}
|
||||
}
|
||||
if tokenclass(tok) != e.class {
|
||||
t.Errorf("bad class for %q: got %d, expected %d", lit, tokenclass(tok), e.class)
|
||||
}
|
||||
epos.Offset += len(lit) + len(e.suf) + len(whitespace)
|
||||
epos.Line += newlineCount(lit) + newlineCount(e.suf) + whitespace_linecount
|
||||
index++
|
||||
if tok == token.EOF {
|
||||
break
|
||||
}
|
||||
if e.suf == "value" {
|
||||
pos, tok, lit = s.Scan()
|
||||
if tok != token.STRING {
|
||||
t.Errorf("bad token for %q: got %q, expected %q", lit, tok, token.STRING)
|
||||
}
|
||||
} else if strings.ContainsRune(e.suf, ';') || strings.ContainsRune(e.suf, '#') {
|
||||
pos, tok, lit = s.Scan()
|
||||
if tok != token.COMMENT {
|
||||
t.Errorf("bad token for %q: got %q, expected %q", lit, tok, token.COMMENT)
|
||||
}
|
||||
}
|
||||
// skip EOLs
|
||||
for i := 0; i < whitespace_linecount+newlineCount(e.suf); i++ {
|
||||
pos, tok, lit = s.Scan()
|
||||
if tok != token.EOL {
|
||||
t.Errorf("bad token for %q: got %q, expected %q", lit, tok, token.EOL)
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.ErrorCount != 0 {
|
||||
t.Errorf("found %d errors", s.ErrorCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanValStringEOF(t *testing.T) {
|
||||
var s Scanner
|
||||
src := "= value"
|
||||
f := fset.AddFile("src", fset.Base(), len(src))
|
||||
s.Init(f, []byte(src), nil, 0)
|
||||
s.Scan() // =
|
||||
s.Scan() // value
|
||||
_, tok, _ := s.Scan() // EOF
|
||||
if tok != token.EOF {
|
||||
t.Errorf("bad token: got %s, expected %s", tok, token.EOF)
|
||||
}
|
||||
if s.ErrorCount > 0 {
|
||||
t.Error("scanning error")
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that initializing the same scanner more then once works correctly.
|
||||
func TestInit(t *testing.T) {
|
||||
var s Scanner
|
||||
|
||||
// 1st init
|
||||
src1 := "\nname = value"
|
||||
f1 := fset.AddFile("src1", fset.Base(), len(src1))
|
||||
s.Init(f1, []byte(src1), nil, 0)
|
||||
if f1.Size() != len(src1) {
|
||||
t.Errorf("bad file size: got %d, expected %d", f1.Size(), len(src1))
|
||||
}
|
||||
s.Scan() // \n
|
||||
s.Scan() // name
|
||||
_, tok, _ := s.Scan() // =
|
||||
if tok != token.ASSIGN {
|
||||
t.Errorf("bad token: got %s, expected %s", tok, token.ASSIGN)
|
||||
}
|
||||
|
||||
// 2nd init
|
||||
src2 := "[section]"
|
||||
f2 := fset.AddFile("src2", fset.Base(), len(src2))
|
||||
s.Init(f2, []byte(src2), nil, 0)
|
||||
if f2.Size() != len(src2) {
|
||||
t.Errorf("bad file size: got %d, expected %d", f2.Size(), len(src2))
|
||||
}
|
||||
_, tok, _ = s.Scan() // [
|
||||
if tok != token.LBRACK {
|
||||
t.Errorf("bad token: got %s, expected %s", tok, token.LBRACK)
|
||||
}
|
||||
|
||||
if s.ErrorCount != 0 {
|
||||
t.Errorf("found %d errors", s.ErrorCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStdErrorHandler(t *testing.T) {
|
||||
const src = "@\n" + // illegal character, cause an error
|
||||
"@ @\n" // two errors on the same line
|
||||
|
||||
var list ErrorList
|
||||
eh := func(pos token.Position, msg string) { list.Add(pos, msg) }
|
||||
|
||||
var s Scanner
|
||||
s.Init(fset.AddFile("File1", fset.Base(), len(src)), []byte(src), eh, 0)
|
||||
for {
|
||||
if _, tok, _ := s.Scan(); tok == token.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(list) != s.ErrorCount {
|
||||
t.Errorf("found %d errors, expected %d", len(list), s.ErrorCount)
|
||||
}
|
||||
|
||||
if len(list) != 3 {
|
||||
t.Errorf("found %d raw errors, expected 3", len(list))
|
||||
PrintError(os.Stderr, list)
|
||||
}
|
||||
|
||||
list.Sort()
|
||||
if len(list) != 3 {
|
||||
t.Errorf("found %d sorted errors, expected 3", len(list))
|
||||
PrintError(os.Stderr, list)
|
||||
}
|
||||
|
||||
list.RemoveMultiples()
|
||||
if len(list) != 2 {
|
||||
t.Errorf("found %d one-per-line errors, expected 2", len(list))
|
||||
PrintError(os.Stderr, list)
|
||||
}
|
||||
}
|
||||
|
||||
type errorCollector struct {
|
||||
cnt int // number of errors encountered
|
||||
msg string // last error message encountered
|
||||
pos token.Position // last error position encountered
|
||||
}
|
||||
|
||||
func checkError(t *testing.T, src string, tok token.Token, pos int, err string) {
|
||||
var s Scanner
|
||||
var h errorCollector
|
||||
eh := func(pos token.Position, msg string) {
|
||||
h.cnt++
|
||||
h.msg = msg
|
||||
h.pos = pos
|
||||
}
|
||||
s.Init(fset.AddFile("", fset.Base(), len(src)), []byte(src), eh, ScanComments)
|
||||
if src[0] == '=' {
|
||||
_, _, _ = s.Scan()
|
||||
}
|
||||
_, tok0, _ := s.Scan()
|
||||
_, tok1, _ := s.Scan()
|
||||
if tok0 != tok {
|
||||
t.Errorf("%q: got %s, expected %s", src, tok0, tok)
|
||||
}
|
||||
if tok1 != token.EOF {
|
||||
t.Errorf("%q: got %s, expected EOF", src, tok1)
|
||||
}
|
||||
cnt := 0
|
||||
if err != "" {
|
||||
cnt = 1
|
||||
}
|
||||
if h.cnt != cnt {
|
||||
t.Errorf("%q: got cnt %d, expected %d", src, h.cnt, cnt)
|
||||
}
|
||||
if h.msg != err {
|
||||
t.Errorf("%q: got msg %q, expected %q", src, h.msg, err)
|
||||
}
|
||||
if h.pos.Offset != pos {
|
||||
t.Errorf("%q: got offset %d, expected %d", src, h.pos.Offset, pos)
|
||||
}
|
||||
}
|
||||
|
||||
var errors = []struct {
|
||||
src string
|
||||
tok token.Token
|
||||
pos int
|
||||
err string
|
||||
}{
|
||||
{"\a", token.ILLEGAL, 0, "illegal character U+0007"},
|
||||
{"/", token.ILLEGAL, 0, "illegal character U+002F '/'"},
|
||||
{"_", token.ILLEGAL, 0, "illegal character U+005F '_'"},
|
||||
{`…`, token.ILLEGAL, 0, "illegal character U+2026 '…'"},
|
||||
{`""`, token.STRING, 0, ""},
|
||||
{`"`, token.STRING, 0, "string not terminated"},
|
||||
{"\"\n", token.STRING, 0, "string not terminated"},
|
||||
{`="`, token.STRING, 1, "string not terminated"},
|
||||
{"=\"\n", token.STRING, 1, "string not terminated"},
|
||||
{"=\\", token.STRING, 1, "unquoted '\\' must be followed by new line"},
|
||||
{"=\\\r", token.STRING, 1, "unquoted '\\' must be followed by new line"},
|
||||
{`"\z"`, token.STRING, 2, "unknown escape sequence"},
|
||||
{`"\a"`, token.STRING, 2, "unknown escape sequence"},
|
||||
{`"\b"`, token.STRING, 2, "unknown escape sequence"},
|
||||
{`"\f"`, token.STRING, 2, "unknown escape sequence"},
|
||||
{`"\r"`, token.STRING, 2, "unknown escape sequence"},
|
||||
{`"\t"`, token.STRING, 2, "unknown escape sequence"},
|
||||
{`"\v"`, token.STRING, 2, "unknown escape sequence"},
|
||||
{`"\0"`, token.STRING, 2, "unknown escape sequence"},
|
||||
}
|
||||
|
||||
func TestScanErrors(t *testing.T) {
|
||||
for _, e := range errors {
|
||||
checkError(t, e.src, e.tok, e.pos, e.err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkScan(b *testing.B) {
|
||||
b.StopTimer()
|
||||
fset := token.NewFileSet()
|
||||
file := fset.AddFile("", fset.Base(), len(source))
|
||||
var s Scanner
|
||||
b.StartTimer()
|
||||
for i := b.N - 1; i >= 0; i-- {
|
||||
s.Init(file, source, nil, ScanComments)
|
||||
for {
|
||||
_, tok, _ := s.Scan()
|
||||
if tok == token.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,281 @@
|
|||
package gcfg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/scalingdata/gcfg/types"
|
||||
)
|
||||
|
||||
type tag struct {
|
||||
ident string
|
||||
intMode string
|
||||
}
|
||||
|
||||
func newTag(ts string) tag {
|
||||
t := tag{}
|
||||
s := strings.Split(ts, ",")
|
||||
t.ident = s[0]
|
||||
for _, tse := range s[1:] {
|
||||
if strings.HasPrefix(tse, "int=") {
|
||||
t.intMode = tse[len("int="):]
|
||||
}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func fieldFold(v reflect.Value, name string) (reflect.Value, tag) {
|
||||
var n string
|
||||
r0, _ := utf8.DecodeRuneInString(name)
|
||||
if unicode.IsLetter(r0) && !unicode.IsLower(r0) && !unicode.IsUpper(r0) {
|
||||
n = "X"
|
||||
}
|
||||
n += strings.Replace(name, "-", "_", -1)
|
||||
f, ok := v.Type().FieldByNameFunc(func(fieldName string) bool {
|
||||
if !v.FieldByName(fieldName).CanSet() {
|
||||
return false
|
||||
}
|
||||
f, _ := v.Type().FieldByName(fieldName)
|
||||
t := newTag(f.Tag.Get("gcfg"))
|
||||
if t.ident != "" {
|
||||
return strings.EqualFold(t.ident, name)
|
||||
}
|
||||
return strings.EqualFold(n, fieldName)
|
||||
})
|
||||
if !ok {
|
||||
return reflect.Value{}, tag{}
|
||||
}
|
||||
return v.FieldByName(f.Name), newTag(f.Tag.Get("gcfg"))
|
||||
}
|
||||
|
||||
type setter func(destp interface{}, blank bool, val string, t tag) error
|
||||
|
||||
var errUnsupportedType = fmt.Errorf("unsupported type")
|
||||
var errBlankUnsupported = fmt.Errorf("blank value not supported for type")
|
||||
|
||||
var setters = []setter{
|
||||
typeSetter, textUnmarshalerSetter, kindSetter, scanSetter,
|
||||
}
|
||||
|
||||
func textUnmarshalerSetter(d interface{}, blank bool, val string, t tag) error {
|
||||
dtu, ok := d.(textUnmarshaler)
|
||||
if !ok {
|
||||
return errUnsupportedType
|
||||
}
|
||||
if blank {
|
||||
return errBlankUnsupported
|
||||
}
|
||||
return dtu.UnmarshalText([]byte(val))
|
||||
}
|
||||
|
||||
func boolSetter(d interface{}, blank bool, val string, t tag) error {
|
||||
if blank {
|
||||
reflect.ValueOf(d).Elem().Set(reflect.ValueOf(true))
|
||||
return nil
|
||||
}
|
||||
b, err := types.ParseBool(val)
|
||||
if err == nil {
|
||||
reflect.ValueOf(d).Elem().Set(reflect.ValueOf(b))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func intMode(mode string) types.IntMode {
|
||||
var m types.IntMode
|
||||
if strings.ContainsAny(mode, "dD") {
|
||||
m |= types.Dec
|
||||
}
|
||||
if strings.ContainsAny(mode, "hH") {
|
||||
m |= types.Hex
|
||||
}
|
||||
if strings.ContainsAny(mode, "oO") {
|
||||
m |= types.Oct
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
var typeModes = map[reflect.Type]types.IntMode{
|
||||
reflect.TypeOf(int(0)): types.Dec | types.Hex,
|
||||
reflect.TypeOf(int8(0)): types.Dec | types.Hex,
|
||||
reflect.TypeOf(int16(0)): types.Dec | types.Hex,
|
||||
reflect.TypeOf(int32(0)): types.Dec | types.Hex,
|
||||
reflect.TypeOf(int64(0)): types.Dec | types.Hex,
|
||||
reflect.TypeOf(uint(0)): types.Dec | types.Hex,
|
||||
reflect.TypeOf(uint8(0)): types.Dec | types.Hex,
|
||||
reflect.TypeOf(uint16(0)): types.Dec | types.Hex,
|
||||
reflect.TypeOf(uint32(0)): types.Dec | types.Hex,
|
||||
reflect.TypeOf(uint64(0)): types.Dec | types.Hex,
|
||||
// use default mode (allow dec/hex/oct) for uintptr type
|
||||
reflect.TypeOf(big.Int{}): types.Dec | types.Hex,
|
||||
}
|
||||
|
||||
func intModeDefault(t reflect.Type) types.IntMode {
|
||||
m, ok := typeModes[t]
|
||||
if !ok {
|
||||
m = types.Dec | types.Hex | types.Oct
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func intSetter(d interface{}, blank bool, val string, t tag) error {
|
||||
if blank {
|
||||
return errBlankUnsupported
|
||||
}
|
||||
mode := intMode(t.intMode)
|
||||
if mode == 0 {
|
||||
mode = intModeDefault(reflect.TypeOf(d).Elem())
|
||||
}
|
||||
return types.ParseInt(d, val, mode)
|
||||
}
|
||||
|
||||
func stringSetter(d interface{}, blank bool, val string, t tag) error {
|
||||
if blank {
|
||||
return errBlankUnsupported
|
||||
}
|
||||
dsp, ok := d.(*string)
|
||||
if !ok {
|
||||
return errUnsupportedType
|
||||
}
|
||||
*dsp = val
|
||||
return nil
|
||||
}
|
||||
|
||||
var kindSetters = map[reflect.Kind]setter{
|
||||
reflect.String: stringSetter,
|
||||
reflect.Bool: boolSetter,
|
||||
reflect.Int: intSetter,
|
||||
reflect.Int8: intSetter,
|
||||
reflect.Int16: intSetter,
|
||||
reflect.Int32: intSetter,
|
||||
reflect.Int64: intSetter,
|
||||
reflect.Uint: intSetter,
|
||||
reflect.Uint8: intSetter,
|
||||
reflect.Uint16: intSetter,
|
||||
reflect.Uint32: intSetter,
|
||||
reflect.Uint64: intSetter,
|
||||
reflect.Uintptr: intSetter,
|
||||
}
|
||||
|
||||
var typeSetters = map[reflect.Type]setter{
|
||||
reflect.TypeOf(big.Int{}): intSetter,
|
||||
}
|
||||
|
||||
func typeSetter(d interface{}, blank bool, val string, tt tag) error {
|
||||
t := reflect.ValueOf(d).Type().Elem()
|
||||
setter, ok := typeSetters[t]
|
||||
if !ok {
|
||||
return errUnsupportedType
|
||||
}
|
||||
return setter(d, blank, val, tt)
|
||||
}
|
||||
|
||||
func kindSetter(d interface{}, blank bool, val string, tt tag) error {
|
||||
k := reflect.ValueOf(d).Type().Elem().Kind()
|
||||
setter, ok := kindSetters[k]
|
||||
if !ok {
|
||||
return errUnsupportedType
|
||||
}
|
||||
return setter(d, blank, val, tt)
|
||||
}
|
||||
|
||||
func scanSetter(d interface{}, blank bool, val string, tt tag) error {
|
||||
if blank {
|
||||
return errBlankUnsupported
|
||||
}
|
||||
return types.ScanFully(d, val, 'v')
|
||||
}
|
||||
|
||||
func set(cfg interface{}, sect, sub, name string, blank bool, value string) error {
|
||||
vPCfg := reflect.ValueOf(cfg)
|
||||
if vPCfg.Kind() != reflect.Ptr || vPCfg.Elem().Kind() != reflect.Struct {
|
||||
panic(fmt.Errorf("config must be a pointer to a struct"))
|
||||
}
|
||||
vCfg := vPCfg.Elem()
|
||||
vSect, _ := fieldFold(vCfg, sect)
|
||||
if !vSect.IsValid() {
|
||||
return fmt.Errorf("invalid section: section %q", sect)
|
||||
}
|
||||
if vSect.Kind() == reflect.Map {
|
||||
vst := vSect.Type()
|
||||
if vst.Key().Kind() != reflect.String ||
|
||||
vst.Elem().Kind() != reflect.Ptr ||
|
||||
vst.Elem().Elem().Kind() != reflect.Struct {
|
||||
panic(fmt.Errorf("map field for section must have string keys and "+
|
||||
" pointer-to-struct values: section %q", sect))
|
||||
}
|
||||
if vSect.IsNil() {
|
||||
vSect.Set(reflect.MakeMap(vst))
|
||||
}
|
||||
k := reflect.ValueOf(sub)
|
||||
pv := vSect.MapIndex(k)
|
||||
if !pv.IsValid() {
|
||||
vType := vSect.Type().Elem().Elem()
|
||||
pv = reflect.New(vType)
|
||||
vSect.SetMapIndex(k, pv)
|
||||
}
|
||||
vSect = pv.Elem()
|
||||
} else if vSect.Kind() != reflect.Struct {
|
||||
panic(fmt.Errorf("field for section must be a map or a struct: "+
|
||||
"section %q", sect))
|
||||
} else if sub != "" {
|
||||
return fmt.Errorf("invalid subsection: "+
|
||||
"section %q subsection %q", sect, sub)
|
||||
}
|
||||
vVar, t := fieldFold(vSect, name)
|
||||
if !vVar.IsValid() {
|
||||
return fmt.Errorf("invalid variable: "+
|
||||
"section %q subsection %q variable %q", sect, sub, name)
|
||||
}
|
||||
// vVal is either single-valued var, or newly allocated value within multi-valued var
|
||||
var vVal reflect.Value
|
||||
// multi-value if unnamed slice type
|
||||
isMulti := vVar.Type().Name() == "" && vVar.Kind() == reflect.Slice
|
||||
if isMulti && blank {
|
||||
vVar.Set(reflect.Zero(vVar.Type()))
|
||||
return nil
|
||||
}
|
||||
if isMulti {
|
||||
vVal = reflect.New(vVar.Type().Elem()).Elem()
|
||||
} else {
|
||||
vVal = vVar
|
||||
}
|
||||
isDeref := vVal.Type().Name() == "" && vVal.Type().Kind() == reflect.Ptr
|
||||
isNew := isDeref && vVal.IsNil()
|
||||
// vAddr is address of value to set (dereferenced & allocated as needed)
|
||||
var vAddr reflect.Value
|
||||
switch {
|
||||
case isNew:
|
||||
vAddr = reflect.New(vVal.Type().Elem())
|
||||
case isDeref && !isNew:
|
||||
vAddr = vVal
|
||||
default:
|
||||
vAddr = vVal.Addr()
|
||||
}
|
||||
vAddrI := vAddr.Interface()
|
||||
err, ok := error(nil), false
|
||||
for _, s := range setters {
|
||||
err = s(vAddrI, blank, value, t)
|
||||
if err == nil {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
if err != errUnsupportedType {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
// in case all setters returned errUnsupportedType
|
||||
return err
|
||||
}
|
||||
if isNew { // set reference if it was dereferenced and newly allocated
|
||||
vVal.Set(vAddr)
|
||||
}
|
||||
if isMulti { // append if multi-valued
|
||||
vVar.Set(reflect.Append(vVar, vVal))
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
; Comment line
|
||||
[section]
|
||||
name=value # comment
|
|
@ -0,0 +1,3 @@
|
|||
; Comment line
|
||||
[甲]
|
||||
乙=丙 # comment
|
|
@ -0,0 +1,435 @@
|
|||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// TODO(gri) consider making this a separate package outside the go directory.
|
||||
|
||||
package token
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Positions
|
||||
|
||||
// Position describes an arbitrary source position
|
||||
// including the file, line, and column location.
|
||||
// A Position is valid if the line number is > 0.
|
||||
//
|
||||
type Position struct {
|
||||
Filename string // filename, if any
|
||||
Offset int // offset, starting at 0
|
||||
Line int // line number, starting at 1
|
||||
Column int // column number, starting at 1 (character count)
|
||||
}
|
||||
|
||||
// IsValid returns true if the position is valid.
|
||||
func (pos *Position) IsValid() bool { return pos.Line > 0 }
|
||||
|
||||
// String returns a string in one of several forms:
|
||||
//
|
||||
// file:line:column valid position with file name
|
||||
// line:column valid position without file name
|
||||
// file invalid position with file name
|
||||
// - invalid position without file name
|
||||
//
|
||||
func (pos Position) String() string {
|
||||
s := pos.Filename
|
||||
if pos.IsValid() {
|
||||
if s != "" {
|
||||
s += ":"
|
||||
}
|
||||
s += fmt.Sprintf("%d:%d", pos.Line, pos.Column)
|
||||
}
|
||||
if s == "" {
|
||||
s = "-"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Pos is a compact encoding of a source position within a file set.
|
||||
// It can be converted into a Position for a more convenient, but much
|
||||
// larger, representation.
|
||||
//
|
||||
// The Pos value for a given file is a number in the range [base, base+size],
|
||||
// where base and size are specified when adding the file to the file set via
|
||||
// AddFile.
|
||||
//
|
||||
// To create the Pos value for a specific source offset, first add
|
||||
// the respective file to the current file set (via FileSet.AddFile)
|
||||
// and then call File.Pos(offset) for that file. Given a Pos value p
|
||||
// for a specific file set fset, the corresponding Position value is
|
||||
// obtained by calling fset.Position(p).
|
||||
//
|
||||
// Pos values can be compared directly with the usual comparison operators:
|
||||
// If two Pos values p and q are in the same file, comparing p and q is
|
||||
// equivalent to comparing the respective source file offsets. If p and q
|
||||
// are in different files, p < q is true if the file implied by p was added
|
||||
// to the respective file set before the file implied by q.
|
||||
//
|
||||
type Pos int
|
||||
|
||||
// The zero value for Pos is NoPos; there is no file and line information
|
||||
// associated with it, and NoPos().IsValid() is false. NoPos is always
|
||||
// smaller than any other Pos value. The corresponding Position value
|
||||
// for NoPos is the zero value for Position.
|
||||
//
|
||||
const NoPos Pos = 0
|
||||
|
||||
// IsValid returns true if the position is valid.
|
||||
func (p Pos) IsValid() bool {
|
||||
return p != NoPos
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// File
|
||||
|
||||
// A File is a handle for a file belonging to a FileSet.
|
||||
// A File has a name, size, and line offset table.
|
||||
//
|
||||
type File struct {
|
||||
set *FileSet
|
||||
name string // file name as provided to AddFile
|
||||
base int // Pos value range for this file is [base...base+size]
|
||||
size int // file size as provided to AddFile
|
||||
|
||||
// lines and infos are protected by set.mutex
|
||||
lines []int
|
||||
infos []lineInfo
|
||||
}
|
||||
|
||||
// Name returns the file name of file f as registered with AddFile.
|
||||
func (f *File) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
// Base returns the base offset of file f as registered with AddFile.
|
||||
func (f *File) Base() int {
|
||||
return f.base
|
||||
}
|
||||
|
||||
// Size returns the size of file f as registered with AddFile.
|
||||
func (f *File) Size() int {
|
||||
return f.size
|
||||
}
|
||||
|
||||
// LineCount returns the number of lines in file f.
|
||||
func (f *File) LineCount() int {
|
||||
f.set.mutex.RLock()
|
||||
n := len(f.lines)
|
||||
f.set.mutex.RUnlock()
|
||||
return n
|
||||
}
|
||||
|
||||
// AddLine adds the line offset for a new line.
|
||||
// The line offset must be larger than the offset for the previous line
|
||||
// and smaller than the file size; otherwise the line offset is ignored.
|
||||
//
|
||||
func (f *File) AddLine(offset int) {
|
||||
f.set.mutex.Lock()
|
||||
if i := len(f.lines); (i == 0 || f.lines[i-1] < offset) && offset < f.size {
|
||||
f.lines = append(f.lines, offset)
|
||||
}
|
||||
f.set.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetLines sets the line offsets for a file and returns true if successful.
|
||||
// The line offsets are the offsets of the first character of each line;
|
||||
// for instance for the content "ab\nc\n" the line offsets are {0, 3}.
|
||||
// An empty file has an empty line offset table.
|
||||
// Each line offset must be larger than the offset for the previous line
|
||||
// and smaller than the file size; otherwise SetLines fails and returns
|
||||
// false.
|
||||
//
|
||||
func (f *File) SetLines(lines []int) bool {
|
||||
// verify validity of lines table
|
||||
size := f.size
|
||||
for i, offset := range lines {
|
||||
if i > 0 && offset <= lines[i-1] || size <= offset {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// set lines table
|
||||
f.set.mutex.Lock()
|
||||
f.lines = lines
|
||||
f.set.mutex.Unlock()
|
||||
return true
|
||||
}
|
||||
|
||||
// SetLinesForContent sets the line offsets for the given file content.
|
||||
func (f *File) SetLinesForContent(content []byte) {
|
||||
var lines []int
|
||||
line := 0
|
||||
for offset, b := range content {
|
||||
if line >= 0 {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
line = -1
|
||||
if b == '\n' {
|
||||
line = offset + 1
|
||||
}
|
||||
}
|
||||
|
||||
// set lines table
|
||||
f.set.mutex.Lock()
|
||||
f.lines = lines
|
||||
f.set.mutex.Unlock()
|
||||
}
|
||||
|
||||
// A lineInfo object describes alternative file and line number
|
||||
// information (such as provided via a //line comment in a .go
|
||||
// file) for a given file offset.
|
||||
type lineInfo struct {
|
||||
// fields are exported to make them accessible to gob
|
||||
Offset int
|
||||
Filename string
|
||||
Line int
|
||||
}
|
||||
|
||||
// AddLineInfo adds alternative file and line number information for
|
||||
// a given file offset. The offset must be larger than the offset for
|
||||
// the previously added alternative line info and smaller than the
|
||||
// file size; otherwise the information is ignored.
|
||||
//
|
||||
// AddLineInfo is typically used to register alternative position
|
||||
// information for //line filename:line comments in source files.
|
||||
//
|
||||
func (f *File) AddLineInfo(offset int, filename string, line int) {
|
||||
f.set.mutex.Lock()
|
||||
if i := len(f.infos); i == 0 || f.infos[i-1].Offset < offset && offset < f.size {
|
||||
f.infos = append(f.infos, lineInfo{offset, filename, line})
|
||||
}
|
||||
f.set.mutex.Unlock()
|
||||
}
|
||||
|
||||
// Pos returns the Pos value for the given file offset;
|
||||
// the offset must be <= f.Size().
|
||||
// f.Pos(f.Offset(p)) == p.
|
||||
//
|
||||
func (f *File) Pos(offset int) Pos {
|
||||
if offset > f.size {
|
||||
panic("illegal file offset")
|
||||
}
|
||||
return Pos(f.base + offset)
|
||||
}
|
||||
|
||||
// Offset returns the offset for the given file position p;
|
||||
// p must be a valid Pos value in that file.
|
||||
// f.Offset(f.Pos(offset)) == offset.
|
||||
//
|
||||
func (f *File) Offset(p Pos) int {
|
||||
if int(p) < f.base || int(p) > f.base+f.size {
|
||||
panic("illegal Pos value")
|
||||
}
|
||||
return int(p) - f.base
|
||||
}
|
||||
|
||||
// Line returns the line number for the given file position p;
|
||||
// p must be a Pos value in that file or NoPos.
|
||||
//
|
||||
func (f *File) Line(p Pos) int {
|
||||
// TODO(gri) this can be implemented much more efficiently
|
||||
return f.Position(p).Line
|
||||
}
|
||||
|
||||
func searchLineInfos(a []lineInfo, x int) int {
|
||||
return sort.Search(len(a), func(i int) bool { return a[i].Offset > x }) - 1
|
||||
}
|
||||
|
||||
// info returns the file name, line, and column number for a file offset.
|
||||
func (f *File) info(offset int) (filename string, line, column int) {
|
||||
filename = f.name
|
||||
if i := searchInts(f.lines, offset); i >= 0 {
|
||||
line, column = i+1, offset-f.lines[i]+1
|
||||
}
|
||||
if len(f.infos) > 0 {
|
||||
// almost no files have extra line infos
|
||||
if i := searchLineInfos(f.infos, offset); i >= 0 {
|
||||
alt := &f.infos[i]
|
||||
filename = alt.Filename
|
||||
if i := searchInts(f.lines, alt.Offset); i >= 0 {
|
||||
line += alt.Line - i - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (f *File) position(p Pos) (pos Position) {
|
||||
offset := int(p) - f.base
|
||||
pos.Offset = offset
|
||||
pos.Filename, pos.Line, pos.Column = f.info(offset)
|
||||
return
|
||||
}
|
||||
|
||||
// Position returns the Position value for the given file position p;
|
||||
// p must be a Pos value in that file or NoPos.
|
||||
//
|
||||
func (f *File) Position(p Pos) (pos Position) {
|
||||
if p != NoPos {
|
||||
if int(p) < f.base || int(p) > f.base+f.size {
|
||||
panic("illegal Pos value")
|
||||
}
|
||||
pos = f.position(p)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// FileSet
|
||||
|
||||
// A FileSet represents a set of source files.
|
||||
// Methods of file sets are synchronized; multiple goroutines
|
||||
// may invoke them concurrently.
|
||||
//
|
||||
type FileSet struct {
|
||||
mutex sync.RWMutex // protects the file set
|
||||
base int // base offset for the next file
|
||||
files []*File // list of files in the order added to the set
|
||||
last *File // cache of last file looked up
|
||||
}
|
||||
|
||||
// NewFileSet creates a new file set.
|
||||
func NewFileSet() *FileSet {
|
||||
s := new(FileSet)
|
||||
s.base = 1 // 0 == NoPos
|
||||
return s
|
||||
}
|
||||
|
||||
// Base returns the minimum base offset that must be provided to
|
||||
// AddFile when adding the next file.
|
||||
//
|
||||
func (s *FileSet) Base() int {
|
||||
s.mutex.RLock()
|
||||
b := s.base
|
||||
s.mutex.RUnlock()
|
||||
return b
|
||||
|
||||
}
|
||||
|
||||
// AddFile adds a new file with a given filename, base offset, and file size
|
||||
// to the file set s and returns the file. Multiple files may have the same
|
||||
// name. The base offset must not be smaller than the FileSet's Base(), and
|
||||
// size must not be negative.
|
||||
//
|
||||
// Adding the file will set the file set's Base() value to base + size + 1
|
||||
// as the minimum base value for the next file. The following relationship
|
||||
// exists between a Pos value p for a given file offset offs:
|
||||
//
|
||||
// int(p) = base + offs
|
||||
//
|
||||
// with offs in the range [0, size] and thus p in the range [base, base+size].
|
||||
// For convenience, File.Pos may be used to create file-specific position
|
||||
// values from a file offset.
|
||||
//
|
||||
func (s *FileSet) AddFile(filename string, base, size int) *File {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
if base < s.base || size < 0 {
|
||||
panic("illegal base or size")
|
||||
}
|
||||
// base >= s.base && size >= 0
|
||||
f := &File{s, filename, base, size, []int{0}, nil}
|
||||
base += size + 1 // +1 because EOF also has a position
|
||||
if base < 0 {
|
||||
panic("token.Pos offset overflow (> 2G of source code in file set)")
|
||||
}
|
||||
// add the file to the file set
|
||||
s.base = base
|
||||
s.files = append(s.files, f)
|
||||
s.last = f
|
||||
return f
|
||||
}
|
||||
|
||||
// Iterate calls f for the files in the file set in the order they were added
|
||||
// until f returns false.
|
||||
//
|
||||
func (s *FileSet) Iterate(f func(*File) bool) {
|
||||
for i := 0; ; i++ {
|
||||
var file *File
|
||||
s.mutex.RLock()
|
||||
if i < len(s.files) {
|
||||
file = s.files[i]
|
||||
}
|
||||
s.mutex.RUnlock()
|
||||
if file == nil || !f(file) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func searchFiles(a []*File, x int) int {
|
||||
return sort.Search(len(a), func(i int) bool { return a[i].base > x }) - 1
|
||||
}
|
||||
|
||||
func (s *FileSet) file(p Pos) *File {
|
||||
// common case: p is in last file
|
||||
if f := s.last; f != nil && f.base <= int(p) && int(p) <= f.base+f.size {
|
||||
return f
|
||||
}
|
||||
// p is not in last file - search all files
|
||||
if i := searchFiles(s.files, int(p)); i >= 0 {
|
||||
f := s.files[i]
|
||||
// f.base <= int(p) by definition of searchFiles
|
||||
if int(p) <= f.base+f.size {
|
||||
s.last = f
|
||||
return f
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// File returns the file that contains the position p.
|
||||
// If no such file is found (for instance for p == NoPos),
|
||||
// the result is nil.
|
||||
//
|
||||
func (s *FileSet) File(p Pos) (f *File) {
|
||||
if p != NoPos {
|
||||
s.mutex.RLock()
|
||||
f = s.file(p)
|
||||
s.mutex.RUnlock()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Position converts a Pos in the fileset into a general Position.
|
||||
func (s *FileSet) Position(p Pos) (pos Position) {
|
||||
if p != NoPos {
|
||||
s.mutex.RLock()
|
||||
if f := s.file(p); f != nil {
|
||||
pos = f.position(p)
|
||||
}
|
||||
s.mutex.RUnlock()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Helper functions
|
||||
|
||||
func searchInts(a []int, x int) int {
|
||||
// This function body is a manually inlined version of:
|
||||
//
|
||||
// return sort.Search(len(a), func(i int) bool { return a[i] > x }) - 1
|
||||
//
|
||||
// With better compiler optimizations, this may not be needed in the
|
||||
// future, but at the moment this change improves the go/printer
|
||||
// benchmark performance by ~30%. This has a direct impact on the
|
||||
// speed of gofmt and thus seems worthwhile (2011-04-29).
|
||||
// TODO(gri): Remove this when compilers have caught up.
|
||||
i, j := 0, len(a)
|
||||
for i < j {
|
||||
h := i + (j-i)/2 // avoid overflow when computing h
|
||||
// i ≤ h < j
|
||||
if a[h] <= x {
|
||||
i = h + 1
|
||||
} else {
|
||||
j = h
|
||||
}
|
||||
}
|
||||
return i - 1
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package token
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func checkPos(t *testing.T, msg string, p, q Position) {
|
||||
if p.Filename != q.Filename {
|
||||
t.Errorf("%s: expected filename = %q; got %q", msg, q.Filename, p.Filename)
|
||||
}
|
||||
if p.Offset != q.Offset {
|
||||
t.Errorf("%s: expected offset = %d; got %d", msg, q.Offset, p.Offset)
|
||||
}
|
||||
if p.Line != q.Line {
|
||||
t.Errorf("%s: expected line = %d; got %d", msg, q.Line, p.Line)
|
||||
}
|
||||
if p.Column != q.Column {
|
||||
t.Errorf("%s: expected column = %d; got %d", msg, q.Column, p.Column)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoPos(t *testing.T) {
|
||||
if NoPos.IsValid() {
|
||||
t.Errorf("NoPos should not be valid")
|
||||
}
|
||||
var fset *FileSet
|
||||
checkPos(t, "nil NoPos", fset.Position(NoPos), Position{})
|
||||
fset = NewFileSet()
|
||||
checkPos(t, "fset NoPos", fset.Position(NoPos), Position{})
|
||||
}
|
||||
|
||||
var tests = []struct {
|
||||
filename string
|
||||
source []byte // may be nil
|
||||
size int
|
||||
lines []int
|
||||
}{
|
||||
{"a", []byte{}, 0, []int{}},
|
||||
{"b", []byte("01234"), 5, []int{0}},
|
||||
{"c", []byte("\n\n\n\n\n\n\n\n\n"), 9, []int{0, 1, 2, 3, 4, 5, 6, 7, 8}},
|
||||
{"d", nil, 100, []int{0, 5, 10, 20, 30, 70, 71, 72, 80, 85, 90, 99}},
|
||||
{"e", nil, 777, []int{0, 80, 100, 120, 130, 180, 267, 455, 500, 567, 620}},
|
||||
{"f", []byte("package p\n\nimport \"fmt\""), 23, []int{0, 10, 11}},
|
||||
{"g", []byte("package p\n\nimport \"fmt\"\n"), 24, []int{0, 10, 11}},
|
||||
{"h", []byte("package p\n\nimport \"fmt\"\n "), 25, []int{0, 10, 11, 24}},
|
||||
}
|
||||
|
||||
func linecol(lines []int, offs int) (int, int) {
|
||||
prevLineOffs := 0
|
||||
for line, lineOffs := range lines {
|
||||
if offs < lineOffs {
|
||||
return line, offs - prevLineOffs + 1
|
||||
}
|
||||
prevLineOffs = lineOffs
|
||||
}
|
||||
return len(lines), offs - prevLineOffs + 1
|
||||
}
|
||||
|
||||
func verifyPositions(t *testing.T, fset *FileSet, f *File, lines []int) {
|
||||
for offs := 0; offs < f.Size(); offs++ {
|
||||
p := f.Pos(offs)
|
||||
offs2 := f.Offset(p)
|
||||
if offs2 != offs {
|
||||
t.Errorf("%s, Offset: expected offset %d; got %d", f.Name(), offs, offs2)
|
||||
}
|
||||
line, col := linecol(lines, offs)
|
||||
msg := fmt.Sprintf("%s (offs = %d, p = %d)", f.Name(), offs, p)
|
||||
checkPos(t, msg, f.Position(f.Pos(offs)), Position{f.Name(), offs, line, col})
|
||||
checkPos(t, msg, fset.Position(p), Position{f.Name(), offs, line, col})
|
||||
}
|
||||
}
|
||||
|
||||
func makeTestSource(size int, lines []int) []byte {
|
||||
src := make([]byte, size)
|
||||
for _, offs := range lines {
|
||||
if offs > 0 {
|
||||
src[offs-1] = '\n'
|
||||
}
|
||||
}
|
||||
return src
|
||||
}
|
||||
|
||||
func TestPositions(t *testing.T) {
|
||||
const delta = 7 // a non-zero base offset increment
|
||||
fset := NewFileSet()
|
||||
for _, test := range tests {
|
||||
// verify consistency of test case
|
||||
if test.source != nil && len(test.source) != test.size {
|
||||
t.Errorf("%s: inconsistent test case: expected file size %d; got %d", test.filename, test.size, len(test.source))
|
||||
}
|
||||
|
||||
// add file and verify name and size
|
||||
f := fset.AddFile(test.filename, fset.Base()+delta, test.size)
|
||||
if f.Name() != test.filename {
|
||||
t.Errorf("expected filename %q; got %q", test.filename, f.Name())
|
||||
}
|
||||
if f.Size() != test.size {
|
||||
t.Errorf("%s: expected file size %d; got %d", f.Name(), test.size, f.Size())
|
||||
}
|
||||
if fset.File(f.Pos(0)) != f {
|
||||
t.Errorf("%s: f.Pos(0) was not found in f", f.Name())
|
||||
}
|
||||
|
||||
// add lines individually and verify all positions
|
||||
for i, offset := range test.lines {
|
||||
f.AddLine(offset)
|
||||
if f.LineCount() != i+1 {
|
||||
t.Errorf("%s, AddLine: expected line count %d; got %d", f.Name(), i+1, f.LineCount())
|
||||
}
|
||||
// adding the same offset again should be ignored
|
||||
f.AddLine(offset)
|
||||
if f.LineCount() != i+1 {
|
||||
t.Errorf("%s, AddLine: expected unchanged line count %d; got %d", f.Name(), i+1, f.LineCount())
|
||||
}
|
||||
verifyPositions(t, fset, f, test.lines[0:i+1])
|
||||
}
|
||||
|
||||
// add lines with SetLines and verify all positions
|
||||
if ok := f.SetLines(test.lines); !ok {
|
||||
t.Errorf("%s: SetLines failed", f.Name())
|
||||
}
|
||||
if f.LineCount() != len(test.lines) {
|
||||
t.Errorf("%s, SetLines: expected line count %d; got %d", f.Name(), len(test.lines), f.LineCount())
|
||||
}
|
||||
verifyPositions(t, fset, f, test.lines)
|
||||
|
||||
// add lines with SetLinesForContent and verify all positions
|
||||
src := test.source
|
||||
if src == nil {
|
||||
// no test source available - create one from scratch
|
||||
src = makeTestSource(test.size, test.lines)
|
||||
}
|
||||
f.SetLinesForContent(src)
|
||||
if f.LineCount() != len(test.lines) {
|
||||
t.Errorf("%s, SetLinesForContent: expected line count %d; got %d", f.Name(), len(test.lines), f.LineCount())
|
||||
}
|
||||
verifyPositions(t, fset, f, test.lines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLineInfo(t *testing.T) {
|
||||
fset := NewFileSet()
|
||||
f := fset.AddFile("foo", fset.Base(), 500)
|
||||
lines := []int{0, 42, 77, 100, 210, 220, 277, 300, 333, 401}
|
||||
// add lines individually and provide alternative line information
|
||||
for _, offs := range lines {
|
||||
f.AddLine(offs)
|
||||
f.AddLineInfo(offs, "bar", 42)
|
||||
}
|
||||
// verify positions for all offsets
|
||||
for offs := 0; offs <= f.Size(); offs++ {
|
||||
p := f.Pos(offs)
|
||||
_, col := linecol(lines, offs)
|
||||
msg := fmt.Sprintf("%s (offs = %d, p = %d)", f.Name(), offs, p)
|
||||
checkPos(t, msg, f.Position(f.Pos(offs)), Position{"bar", offs, 42, col})
|
||||
checkPos(t, msg, fset.Position(p), Position{"bar", offs, 42, col})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFiles(t *testing.T) {
|
||||
fset := NewFileSet()
|
||||
for i, test := range tests {
|
||||
fset.AddFile(test.filename, fset.Base(), test.size)
|
||||
j := 0
|
||||
fset.Iterate(func(f *File) bool {
|
||||
if f.Name() != tests[j].filename {
|
||||
t.Errorf("expected filename = %s; got %s", tests[j].filename, f.Name())
|
||||
}
|
||||
j++
|
||||
return true
|
||||
})
|
||||
if j != i+1 {
|
||||
t.Errorf("expected %d files; got %d", i+1, j)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package token
|
||||
|
||||
type serializedFile struct {
|
||||
// fields correspond 1:1 to fields with same (lower-case) name in File
|
||||
Name string
|
||||
Base int
|
||||
Size int
|
||||
Lines []int
|
||||
Infos []lineInfo
|
||||
}
|
||||
|
||||
type serializedFileSet struct {
|
||||
Base int
|
||||
Files []serializedFile
|
||||
}
|
||||
|
||||
// Read calls decode to deserialize a file set into s; s must not be nil.
|
||||
func (s *FileSet) Read(decode func(interface{}) error) error {
|
||||
var ss serializedFileSet
|
||||
if err := decode(&ss); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.mutex.Lock()
|
||||
s.base = ss.Base
|
||||
files := make([]*File, len(ss.Files))
|
||||
for i := 0; i < len(ss.Files); i++ {
|
||||
f := &ss.Files[i]
|
||||
files[i] = &File{s, f.Name, f.Base, f.Size, f.Lines, f.Infos}
|
||||
}
|
||||
s.files = files
|
||||
s.last = nil
|
||||
s.mutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write calls encode to serialize the file set s.
|
||||
func (s *FileSet) Write(encode func(interface{}) error) error {
|
||||
var ss serializedFileSet
|
||||
|
||||
s.mutex.Lock()
|
||||
ss.Base = s.base
|
||||
files := make([]serializedFile, len(s.files))
|
||||
for i, f := range s.files {
|
||||
files[i] = serializedFile{f.name, f.base, f.size, f.lines, f.infos}
|
||||
}
|
||||
ss.Files = files
|
||||
s.mutex.Unlock()
|
||||
|
||||
return encode(ss)
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package token
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// equal returns nil if p and q describe the same file set;
|
||||
// otherwise it returns an error describing the discrepancy.
|
||||
func equal(p, q *FileSet) error {
|
||||
if p == q {
|
||||
// avoid deadlock if p == q
|
||||
return nil
|
||||
}
|
||||
|
||||
// not strictly needed for the test
|
||||
p.mutex.Lock()
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
if p.base != q.base {
|
||||
return fmt.Errorf("different bases: %d != %d", p.base, q.base)
|
||||
}
|
||||
|
||||
if len(p.files) != len(q.files) {
|
||||
return fmt.Errorf("different number of files: %d != %d", len(p.files), len(q.files))
|
||||
}
|
||||
|
||||
for i, f := range p.files {
|
||||
g := q.files[i]
|
||||
if f.set != p {
|
||||
return fmt.Errorf("wrong fileset for %q", f.name)
|
||||
}
|
||||
if g.set != q {
|
||||
return fmt.Errorf("wrong fileset for %q", g.name)
|
||||
}
|
||||
if f.name != g.name {
|
||||
return fmt.Errorf("different filenames: %q != %q", f.name, g.name)
|
||||
}
|
||||
if f.base != g.base {
|
||||
return fmt.Errorf("different base for %q: %d != %d", f.name, f.base, g.base)
|
||||
}
|
||||
if f.size != g.size {
|
||||
return fmt.Errorf("different size for %q: %d != %d", f.name, f.size, g.size)
|
||||
}
|
||||
for j, l := range f.lines {
|
||||
m := g.lines[j]
|
||||
if l != m {
|
||||
return fmt.Errorf("different offsets for %q", f.name)
|
||||
}
|
||||
}
|
||||
for j, l := range f.infos {
|
||||
m := g.infos[j]
|
||||
if l.Offset != m.Offset || l.Filename != m.Filename || l.Line != m.Line {
|
||||
return fmt.Errorf("different infos for %q", f.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we don't care about .last - it's just a cache
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkSerialize(t *testing.T, p *FileSet) {
|
||||
var buf bytes.Buffer
|
||||
encode := func(x interface{}) error {
|
||||
return gob.NewEncoder(&buf).Encode(x)
|
||||
}
|
||||
if err := p.Write(encode); err != nil {
|
||||
t.Errorf("writing fileset failed: %s", err)
|
||||
return
|
||||
}
|
||||
q := NewFileSet()
|
||||
decode := func(x interface{}) error {
|
||||
return gob.NewDecoder(&buf).Decode(x)
|
||||
}
|
||||
if err := q.Read(decode); err != nil {
|
||||
t.Errorf("reading fileset failed: %s", err)
|
||||
return
|
||||
}
|
||||
if err := equal(p, q); err != nil {
|
||||
t.Errorf("filesets not identical: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSerialization(t *testing.T) {
|
||||
p := NewFileSet()
|
||||
checkSerialize(t, p)
|
||||
// add some files
|
||||
for i := 0; i < 10; i++ {
|
||||
f := p.AddFile(fmt.Sprintf("file%d", i), p.Base()+i, i*100)
|
||||
checkSerialize(t, p)
|
||||
// add some lines and alternative file infos
|
||||
line := 1000
|
||||
for offs := 0; offs < f.Size(); offs += 40 + i {
|
||||
f.AddLine(offs)
|
||||
if offs%7 == 0 {
|
||||
f.AddLineInfo(offs, fmt.Sprintf("file%d", offs), line)
|
||||
line += 33
|
||||
}
|
||||
}
|
||||
checkSerialize(t, p)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package token defines constants representing the lexical tokens of the gcfg
|
||||
// configuration syntax and basic operations on tokens (printing, predicates).
|
||||
//
|
||||
// Note that the API for the token package may change to accommodate new
|
||||
// features or implementation changes in gcfg.
|
||||
//
|
||||
package token
|
||||
|
||||
import "strconv"
|
||||
|
||||
// Token is the set of lexical tokens of the gcfg configuration syntax.
|
||||
type Token int
|
||||
|
||||
// The list of tokens.
|
||||
const (
|
||||
// Special tokens
|
||||
ILLEGAL Token = iota
|
||||
EOF
|
||||
COMMENT
|
||||
|
||||
literal_beg
|
||||
// Identifiers and basic type literals
|
||||
// (these tokens stand for classes of literals)
|
||||
IDENT // section-name, variable-name
|
||||
STRING // "subsection-name", variable value
|
||||
literal_end
|
||||
|
||||
operator_beg
|
||||
// Operators and delimiters
|
||||
ASSIGN // =
|
||||
LBRACK // [
|
||||
RBRACK // ]
|
||||
EOL // \n
|
||||
operator_end
|
||||
)
|
||||
|
||||
var tokens = [...]string{
|
||||
ILLEGAL: "ILLEGAL",
|
||||
|
||||
EOF: "EOF",
|
||||
COMMENT: "COMMENT",
|
||||
|
||||
IDENT: "IDENT",
|
||||
STRING: "STRING",
|
||||
|
||||
ASSIGN: "=",
|
||||
LBRACK: "[",
|
||||
RBRACK: "]",
|
||||
EOL: "\n",
|
||||
}
|
||||
|
||||
// String returns the string corresponding to the token tok.
|
||||
// For operators and delimiters, the string is the actual token character
|
||||
// sequence (e.g., for the token ASSIGN, the string is "="). For all other
|
||||
// tokens the string corresponds to the token constant name (e.g. for the
|
||||
// token IDENT, the string is "IDENT").
|
||||
//
|
||||
func (tok Token) String() string {
|
||||
s := ""
|
||||
if 0 <= tok && tok < Token(len(tokens)) {
|
||||
s = tokens[tok]
|
||||
}
|
||||
if s == "" {
|
||||
s = "token(" + strconv.Itoa(int(tok)) + ")"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Predicates
|
||||
|
||||
// IsLiteral returns true for tokens corresponding to identifiers
|
||||
// and basic type literals; it returns false otherwise.
|
||||
//
|
||||
func (tok Token) IsLiteral() bool { return literal_beg < tok && tok < literal_end }
|
||||
|
||||
// IsOperator returns true for tokens corresponding to operators and
|
||||
// delimiters; it returns false otherwise.
|
||||
//
|
||||
func (tok Token) IsOperator() bool { return operator_beg < tok && tok < operator_end }
|
|
@ -0,0 +1,23 @@
|
|||
package types
|
||||
|
||||
// BoolValues defines the name and value mappings for ParseBool.
|
||||
var BoolValues = map[string]interface{}{
|
||||
"true": true, "yes": true, "on": true, "1": true,
|
||||
"false": false, "no": false, "off": false, "0": false,
|
||||
}
|
||||
|
||||
var boolParser = func() *EnumParser {
|
||||
ep := &EnumParser{}
|
||||
ep.AddVals(BoolValues)
|
||||
return ep
|
||||
}()
|
||||
|
||||
// ParseBool parses bool values according to the definitions in BoolValues.
|
||||
// Parsing is case-insensitive.
|
||||
func ParseBool(s string) (bool, error) {
|
||||
v, err := boolParser.Parse(s)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return v.(bool), nil
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
// Package types defines helpers for type conversions.
|
||||
//
|
||||
// The API for this package is not finalized yet.
|
||||
package types
|
|
@ -0,0 +1,44 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// EnumParser parses "enum" values; i.e. a predefined set of strings to
|
||||
// predefined values.
|
||||
type EnumParser struct {
|
||||
Type string // type name; if not set, use type of first value added
|
||||
CaseMatch bool // if true, matching of strings is case-sensitive
|
||||
// PrefixMatch bool
|
||||
vals map[string]interface{}
|
||||
}
|
||||
|
||||
// AddVals adds strings and values to an EnumParser.
|
||||
func (ep *EnumParser) AddVals(vals map[string]interface{}) {
|
||||
if ep.vals == nil {
|
||||
ep.vals = make(map[string]interface{})
|
||||
}
|
||||
for k, v := range vals {
|
||||
if ep.Type == "" {
|
||||
ep.Type = reflect.TypeOf(v).Name()
|
||||
}
|
||||
if !ep.CaseMatch {
|
||||
k = strings.ToLower(k)
|
||||
}
|
||||
ep.vals[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the string and returns the value or an error.
|
||||
func (ep EnumParser) Parse(s string) (interface{}, error) {
|
||||
if !ep.CaseMatch {
|
||||
s = strings.ToLower(s)
|
||||
}
|
||||
v, ok := ep.vals[s]
|
||||
if !ok {
|
||||
return false, fmt.Errorf("failed to parse %s %#q", ep.Type, s)
|
||||
}
|
||||
return v, nil
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEnumParserBool(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
val string
|
||||
res bool
|
||||
ok bool
|
||||
}{
|
||||
{val: "tRuE", res: true, ok: true},
|
||||
{val: "False", res: false, ok: true},
|
||||
{val: "t", ok: false},
|
||||
} {
|
||||
b, err := ParseBool(tt.val)
|
||||
switch {
|
||||
case tt.ok && err != nil:
|
||||
t.Errorf("%q: got error %v, want %v", tt.val, err, tt.res)
|
||||
case !tt.ok && err == nil:
|
||||
t.Errorf("%q: got %v, want error", tt.val, b)
|
||||
case tt.ok && b != tt.res:
|
||||
t.Errorf("%q: got %v, want %v", tt.val, b, tt.res)
|
||||
default:
|
||||
t.Logf("%q: got %v, %v", tt.val, b, err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// An IntMode is a mode for parsing integer values, representing a set of
|
||||
// accepted bases.
|
||||
type IntMode uint8
|
||||
|
||||
// IntMode values for ParseInt; can be combined using binary or.
|
||||
const (
|
||||
Dec IntMode = 1 << iota
|
||||
Hex
|
||||
Oct
|
||||
)
|
||||
|
||||
// String returns a string representation of IntMode; e.g. `IntMode(Dec|Hex)`.
|
||||
func (m IntMode) String() string {
|
||||
var modes []string
|
||||
if m&Dec != 0 {
|
||||
modes = append(modes, "Dec")
|
||||
}
|
||||
if m&Hex != 0 {
|
||||
modes = append(modes, "Hex")
|
||||
}
|
||||
if m&Oct != 0 {
|
||||
modes = append(modes, "Oct")
|
||||
}
|
||||
return "IntMode(" + strings.Join(modes, "|") + ")"
|
||||
}
|
||||
|
||||
var errIntAmbig = fmt.Errorf("ambiguous integer value; must include '0' prefix")
|
||||
|
||||
func prefix0(val string) bool {
|
||||
return strings.HasPrefix(val, "0") || strings.HasPrefix(val, "-0")
|
||||
}
|
||||
|
||||
func prefix0x(val string) bool {
|
||||
return strings.HasPrefix(val, "0x") || strings.HasPrefix(val, "-0x")
|
||||
}
|
||||
|
||||
// ParseInt parses val using mode into intptr, which must be a pointer to an
|
||||
// integer kind type. Non-decimal value require prefix `0` or `0x` in the cases
|
||||
// when mode permits ambiguity of base; otherwise the prefix can be omitted.
|
||||
func ParseInt(intptr interface{}, val string, mode IntMode) error {
|
||||
val = strings.TrimSpace(val)
|
||||
verb := byte(0)
|
||||
switch mode {
|
||||
case Dec:
|
||||
verb = 'd'
|
||||
case Dec + Hex:
|
||||
if prefix0x(val) {
|
||||
verb = 'v'
|
||||
} else {
|
||||
verb = 'd'
|
||||
}
|
||||
case Dec + Oct:
|
||||
if prefix0(val) && !prefix0x(val) {
|
||||
verb = 'v'
|
||||
} else {
|
||||
verb = 'd'
|
||||
}
|
||||
case Dec + Hex + Oct:
|
||||
verb = 'v'
|
||||
case Hex:
|
||||
if prefix0x(val) {
|
||||
verb = 'v'
|
||||
} else {
|
||||
verb = 'x'
|
||||
}
|
||||
case Oct:
|
||||
verb = 'o'
|
||||
case Hex + Oct:
|
||||
if prefix0(val) {
|
||||
verb = 'v'
|
||||
} else {
|
||||
return errIntAmbig
|
||||
}
|
||||
}
|
||||
if verb == 0 {
|
||||
panic("unsupported mode")
|
||||
}
|
||||
return ScanFully(intptr, val, verb)
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func elem(p interface{}) interface{} {
|
||||
return reflect.ValueOf(p).Elem().Interface()
|
||||
}
|
||||
|
||||
func TestParseInt(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
val string
|
||||
mode IntMode
|
||||
exp interface{}
|
||||
ok bool
|
||||
}{
|
||||
{"0", Dec, int(0), true},
|
||||
{"10", Dec, int(10), true},
|
||||
{"-10", Dec, int(-10), true},
|
||||
{"x", Dec, int(0), false},
|
||||
{"0xa", Hex, int(0xa), true},
|
||||
{"a", Hex, int(0xa), true},
|
||||
{"10", Hex, int(0x10), true},
|
||||
{"-0xa", Hex, int(-0xa), true},
|
||||
{"0x", Hex, int(0x0), true}, // Scanf doesn't require digit behind 0x
|
||||
{"-0x", Hex, int(0x0), true}, // Scanf doesn't require digit behind 0x
|
||||
{"-a", Hex, int(-0xa), true},
|
||||
{"-10", Hex, int(-0x10), true},
|
||||
{"x", Hex, int(0), false},
|
||||
{"10", Oct, int(010), true},
|
||||
{"010", Oct, int(010), true},
|
||||
{"-10", Oct, int(-010), true},
|
||||
{"-010", Oct, int(-010), true},
|
||||
{"10", Dec | Hex, int(10), true},
|
||||
{"010", Dec | Hex, int(10), true},
|
||||
{"0x10", Dec | Hex, int(0x10), true},
|
||||
{"10", Dec | Oct, int(10), true},
|
||||
{"010", Dec | Oct, int(010), true},
|
||||
{"0x10", Dec | Oct, int(0), false},
|
||||
{"10", Hex | Oct, int(0), false}, // need prefix to distinguish Hex/Oct
|
||||
{"010", Hex | Oct, int(010), true},
|
||||
{"0x10", Hex | Oct, int(0x10), true},
|
||||
{"10", Dec | Hex | Oct, int(10), true},
|
||||
{"010", Dec | Hex | Oct, int(010), true},
|
||||
{"0x10", Dec | Hex | Oct, int(0x10), true},
|
||||
} {
|
||||
typ := reflect.TypeOf(tt.exp)
|
||||
res := reflect.New(typ).Interface()
|
||||
err := ParseInt(res, tt.val, tt.mode)
|
||||
switch {
|
||||
case tt.ok && err != nil:
|
||||
t.Errorf("ParseInt(%v, %#v, %v): fail; got error %v, want ok",
|
||||
typ, tt.val, tt.mode, err)
|
||||
case !tt.ok && err == nil:
|
||||
t.Errorf("ParseInt(%v, %#v, %v): fail; got %v, want error",
|
||||
typ, tt.val, tt.mode, elem(res))
|
||||
case tt.ok && !reflect.DeepEqual(elem(res), tt.exp):
|
||||
t.Errorf("ParseInt(%v, %#v, %v): fail; got %v, want %v",
|
||||
typ, tt.val, tt.mode, elem(res), tt.exp)
|
||||
default:
|
||||
t.Logf("ParseInt(%v, %#v, %s): pass; got %v, error %v",
|
||||
typ, tt.val, tt.mode, elem(res), err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// ScanFully uses fmt.Sscanf with verb to fully scan val into ptr.
|
||||
func ScanFully(ptr interface{}, val string, verb byte) error {
|
||||
t := reflect.ValueOf(ptr).Elem().Type()
|
||||
// attempt to read extra bytes to make sure the value is consumed
|
||||
var b []byte
|
||||
n, err := fmt.Sscanf(val, "%"+string(verb)+"%s", ptr, &b)
|
||||
switch {
|
||||
case n < 1 || n == 1 && err != io.EOF:
|
||||
return fmt.Errorf("failed to parse %q as %v: %v", val, t, err)
|
||||
case n > 1:
|
||||
return fmt.Errorf("failed to parse %q as %v: extra characters %q", val, t, string(b))
|
||||
}
|
||||
// n == 1 && err == io.EOF
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestScanFully(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
val string
|
||||
verb byte
|
||||
res interface{}
|
||||
ok bool
|
||||
}{
|
||||
{"a", 'v', int(0), false},
|
||||
{"0x", 'v', int(0), true},
|
||||
{"0x", 'd', int(0), false},
|
||||
} {
|
||||
d := reflect.New(reflect.TypeOf(tt.res)).Interface()
|
||||
err := ScanFully(d, tt.val, tt.verb)
|
||||
switch {
|
||||
case tt.ok && err != nil:
|
||||
t.Errorf("ScanFully(%T, %q, '%c'): want ok, got error %v",
|
||||
d, tt.val, tt.verb, err)
|
||||
case !tt.ok && err == nil:
|
||||
t.Errorf("ScanFully(%T, %q, '%c'): want error, got %v",
|
||||
d, tt.val, tt.verb, elem(d))
|
||||
case tt.ok && err == nil && !reflect.DeepEqual(tt.res, elem(d)):
|
||||
t.Errorf("ScanFully(%T, %q, '%c'): want %v, got %v",
|
||||
d, tt.val, tt.verb, tt.res, elem(d))
|
||||
default:
|
||||
t.Logf("ScanFully(%T, %q, '%c') = %v; *ptr==%v",
|
||||
d, tt.val, tt.verb, err, elem(d))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Yusuke Inuzuka
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,10 @@
|
|||
.PHONY: build test
|
||||
|
||||
build:
|
||||
./_tools/go-inline *.go && go fmt . && go build
|
||||
|
||||
glua: *.go pm/*.go cmd/glua/glua.go
|
||||
./_tools/go-inline *.go && go fmt . && go build cmd/glua/glua.go
|
||||
|
||||
test:
|
||||
./_tools/go-inline *.go && go fmt . && go test
|
|
@ -0,0 +1,708 @@
|
|||
===============================================================================
|
||||
GopherLua: VM and compiler for Lua in Go.
|
||||
===============================================================================
|
||||
|
||||
.. image:: https://godoc.org/github.com/yuin/gopher-lua?status.svg
|
||||
:target: http://godoc.org/github.com/yuin/gopher-lua
|
||||
|
||||
.. image:: https://travis-ci.org/yuin/gopher-lua.svg
|
||||
:target: https://travis-ci.org/yuin/gopher-lua
|
||||
|
||||
.. image:: https://coveralls.io/repos/yuin/gopher-lua/badge.svg
|
||||
:target: https://coveralls.io/r/yuin/gopher-lua
|
||||
|
||||
.. image:: https://badges.gitter.im/Join%20Chat.svg
|
||||
:alt: Join the chat at https://gitter.im/yuin/gopher-lua
|
||||
:target: https://gitter.im/yuin/gopher-lua?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
|
||||
|
|
||||
|
||||
GopherLua is a Lua5.1 VM and compiler written in Go. GopherLua has a same goal
|
||||
with Lua: **Be a scripting language with extensible semantics** . It provides
|
||||
Go APIs that allow you to easily embed a scripting language to your Go host
|
||||
programs.
|
||||
|
||||
.. contents::
|
||||
:depth: 1
|
||||
|
||||
----------------------------------------------------------------
|
||||
Design principle
|
||||
----------------------------------------------------------------
|
||||
|
||||
- Be a scripting language with extensible semantics.
|
||||
- User-friendly Go API
|
||||
- The stack based API like the one used in the original Lua
|
||||
implementation will cause a performance improvements in GopherLua
|
||||
(It will reduce memory allocations and concrete type <-> interface conversions).
|
||||
GopherLua API is **not** the stack based API.
|
||||
GopherLua give preference to the user-friendliness over the performance.
|
||||
|
||||
----------------------------------------------------------------
|
||||
How about performance?
|
||||
----------------------------------------------------------------
|
||||
GopherLua is not fast but not too slow, I think.
|
||||
|
||||
There are some benchmarks on the `wiki page <https://github.com/yuin/gopher-lua/wiki/Benchmarks>`_ .
|
||||
|
||||
----------------------------------------------------------------
|
||||
Installation
|
||||
----------------------------------------------------------------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
go get github.com/yuin/gopher-lua
|
||||
|
||||
GopherLua supports >= Go1.4.
|
||||
|
||||
----------------------------------------------------------------
|
||||
Usage
|
||||
----------------------------------------------------------------
|
||||
GopherLua APIs perform in much the same way as Lua, **but the stack is used only
|
||||
for passing arguments and receiving returned values.**
|
||||
|
||||
GopherLua supports channel operations. See **"Goroutines"** section.
|
||||
|
||||
Import a package.
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
import (
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
Run scripts in the VM.
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
if err := L.DoString(`print("hello")`); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
if err := L.DoFile("hello.lua"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
Refer to `Lua Reference Manual <http://www.lua.org/manual/5.1/>`_ and `Go doc <http://godoc.org/github.com/yuin/gopher-lua>`_ for further information.
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Data model
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
All data in a GopherLua program is an ``LValue`` . ``LValue`` is an interface
|
||||
type that has following methods.
|
||||
|
||||
- ``String() string``
|
||||
- ``Type() LValueType``
|
||||
|
||||
|
||||
Objects implement an LValue interface are
|
||||
|
||||
================ ========================= ================== =======================
|
||||
Type name Go type Type() value Constants
|
||||
================ ========================= ================== =======================
|
||||
``LNilType`` (constants) ``LTNil`` ``LNil``
|
||||
``LBool`` (constants) ``LTBool`` ``LTrue``, ``LFalse``
|
||||
``LNumber`` float64 ``LTNumber`` ``-``
|
||||
``LString`` string ``LTString`` ``-``
|
||||
``LFunction`` struct pointer ``LTFunction`` ``-``
|
||||
``LUserData`` struct pointer ``LTUserData`` ``-``
|
||||
``LState`` struct pointer ``LTThread`` ``-``
|
||||
``LTable`` struct pointer ``LTTable`` ``-``
|
||||
``LChannel`` chan LValue ``LTChannel`` ``-``
|
||||
================ ========================= ================== =======================
|
||||
|
||||
You can test an object type in Go way(type assertion) or using a ``Type()`` value.
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
lv := L.Get(-1) // get the value at the top of the stack
|
||||
if str, ok := lv.(lua.LString); ok {
|
||||
// lv is LString
|
||||
fmt.Println(string(str))
|
||||
}
|
||||
if lv.Type() != lua.LTString {
|
||||
panic("string required.")
|
||||
}
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
lv := L.Get(-1) // get the value at the top of the stack
|
||||
if tbl, ok := lv.(*lua.LTable); ok {
|
||||
// lv is LTable
|
||||
fmt.Println(L.ObjLen(tbl))
|
||||
}
|
||||
|
||||
Note that ``LBool`` , ``LNumber`` , ``LString`` is not a pointer.
|
||||
|
||||
To test ``LNilType`` and ``LBool``, You **must** use pre-defined constants.
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
lv := L.Get(-1) // get the value at the top of the stack
|
||||
|
||||
if lv == LTrue { // correct
|
||||
}
|
||||
|
||||
if bl, ok == lv.(lua.LBool); ok && bool(bl) { // wrong
|
||||
}
|
||||
|
||||
In Lua, both ``nil`` and ``false`` make a condition false. ``LVIsFalse`` and ``LVAsBool`` implement this specification.
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
lv := L.Get(-1) // get the value at the top of the stack
|
||||
if LVIsFalse(lv) { // lv is nil or false
|
||||
}
|
||||
|
||||
if LVAsBool(lv) { // lv is neither nil nor false
|
||||
}
|
||||
|
||||
Objects that based on go structs(``LFunction``. ``LUserData``, ``LTable``)
|
||||
have some public methods and fields. You can use these methods and fields for
|
||||
performance and debugging, but there are some limitations.
|
||||
|
||||
- Metatable does not work.
|
||||
- No error handlings.
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Callstack & Registry size
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Size of the callstack & registry is **fixed** for mainly performance.
|
||||
You can change the default size of the callstack & registry.
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
lua.RegistrySize = 1024 * 20
|
||||
lua.CallStackSize = 1024
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
You can also create an LState object that has the callstack & registry size specified by ``Options`` .
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
L := lua.NewState(lua.Options{
|
||||
CallStackSize: 120,
|
||||
RegistrySize: 120*20,
|
||||
})
|
||||
|
||||
An LState object that has been created by ``*LState#NewThread()`` inherits the callstack & registry size from the parent LState object.
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
API
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Refer to `Lua Reference Manual <http://www.lua.org/manual/5.1/>`_ and `Go doc(LState methods) <http://godoc.org/github.com/yuin/gopher-lua>`_ for further information.
|
||||
|
||||
+++++++++++++++++++++++++++++++++++++++++
|
||||
Calling Go from Lua
|
||||
+++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
func Double(L *lua.LState) int {
|
||||
lv := L.ToInt(1) /* get argument */
|
||||
L.Push(lua.LNumber(lv * 2)) /* push result */
|
||||
return 1 /* number of results */
|
||||
}
|
||||
|
||||
func main() {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
L.SetGlobal("double", L.NewFunction(Double)) /* Original lua_setglobal uses stack... */
|
||||
}
|
||||
|
||||
.. code-block:: lua
|
||||
|
||||
print(double(20)) -- > "40"
|
||||
|
||||
Any function registered with GopherLua is a ``lua.LGFunction``, defined in ``value.go``
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
type LGFunction func(*LState) int
|
||||
|
||||
Working with coroutines.
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
co := L.NewThread() /* create a new thread */
|
||||
fn := L.GetGlobal("coro").(*lua.LFunction) /* get function from lua */
|
||||
for {
|
||||
st, err, values := L.Resume(co, fn)
|
||||
if st == lua.ResumeError {
|
||||
fmt.Println("yield break(error)")
|
||||
fmt.Println(err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
for i, lv := range values {
|
||||
fmt.Printf("%v : %v\n", i, lv)
|
||||
}
|
||||
|
||||
if st == lua.ResumeOK {
|
||||
fmt.Println("yield break(ok)")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
+++++++++++++++++++++++++++++++++++++++++
|
||||
Creating a module by Go
|
||||
+++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
mymodule.go
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
package mymodule
|
||||
|
||||
import (
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func Loader(L *lua.LState) int {
|
||||
// register functions to the table
|
||||
mod := L.SetFuncs(L.NewTable(), exports)
|
||||
// register other stuff
|
||||
L.SetField(mod, "name", lua.LString("value"))
|
||||
|
||||
// returns the module
|
||||
L.Push(mod)
|
||||
return 1
|
||||
}
|
||||
|
||||
var exports = map[string]lua.LGFunction{
|
||||
"myfunc": myfunc,
|
||||
}
|
||||
|
||||
func myfunc(L *lua.LState) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
mymain.go
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"./mymodule"
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func main() {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
L.PreloadModule("mymodule", mymodule.Loader)
|
||||
if err := L.DoFile("main.lua"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
main.lua
|
||||
|
||||
.. code-block:: lua
|
||||
|
||||
local m = require("mymodule")
|
||||
m.myfunc()
|
||||
print(m.name)
|
||||
|
||||
|
||||
+++++++++++++++++++++++++++++++++++++++++
|
||||
Calling Lua from Go
|
||||
+++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
if err := L.DoFile("double.lua"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := L.CallByParam(lua.P{
|
||||
Fn: L.GetGlobal("double"),
|
||||
NRet: 1,
|
||||
Protect: true,
|
||||
}, lua.LNumber(10)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ret := L.Get(-1) // returned value
|
||||
L.Pop(1) // remove received value
|
||||
|
||||
If ``Protect`` is false, GopherLua will panic instead of returning an ``error`` value.
|
||||
|
||||
+++++++++++++++++++++++++++++++++++++++++
|
||||
User-Defined types
|
||||
+++++++++++++++++++++++++++++++++++++++++
|
||||
You can extend GopherLua with new types written in Go.
|
||||
``LUserData`` is provided for this purpose.
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
const luaPersonTypeName = "person"
|
||||
|
||||
// Registers my person type to given L.
|
||||
func registerPersonType(L *lua.LState) {
|
||||
mt := L.NewTypeMetatable(luaPersonTypeName)
|
||||
L.SetGlobal("person", mt)
|
||||
// static attributes
|
||||
L.SetField(mt, "new", L.NewFunction(newPerson))
|
||||
// methods
|
||||
L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), personMethods))
|
||||
}
|
||||
|
||||
// Constructor
|
||||
func newPerson(L *lua.LState) int {
|
||||
person := &Person{L.CheckString(1)}
|
||||
ud := L.NewUserData()
|
||||
ud.Value = person
|
||||
L.SetMetatable(ud, L.GetTypeMetatable(luaPersonTypeName))
|
||||
L.Push(ud)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Checks whether the first lua argument is a *LUserData with *Person and returns this *Person.
|
||||
func checkPerson(L *lua.LState) *Person {
|
||||
ud := L.CheckUserData(1)
|
||||
if v, ok := ud.Value.(*Person); ok {
|
||||
return v
|
||||
}
|
||||
L.ArgError(1, "person expected")
|
||||
return nil
|
||||
}
|
||||
|
||||
var personMethods = map[string]lua.LGFunction{
|
||||
"name": personGetSetName,
|
||||
}
|
||||
|
||||
// Getter and setter for the Person#Name
|
||||
func personGetSetName(L *lua.LState) int {
|
||||
p := checkPerson(L)
|
||||
if L.GetTop() == 2 {
|
||||
p.Name = L.CheckString(2)
|
||||
return 0
|
||||
}
|
||||
L.Push(lua.LString(p.Name))
|
||||
return 1
|
||||
}
|
||||
|
||||
func main() {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
registerPersonType(L)
|
||||
if err := L.DoString(`
|
||||
p = person.new("Steeve")
|
||||
print(p:name()) -- "Steeve"
|
||||
p:name("Alice")
|
||||
print(p:name()) -- "Alice"
|
||||
`); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
+++++++++++++++++++++++++++++++++++++++++
|
||||
Goroutines
|
||||
+++++++++++++++++++++++++++++++++++++++++
|
||||
The ``LState`` is not goroutine-safe. It is recommended to use one LState per goroutine and communicate between goroutines by using channels.
|
||||
|
||||
Channels are represented by ``channel`` objects in GopherLua. And a ``channel`` table provides functions for performing channel operations.
|
||||
|
||||
Some objects can not be sent over channels due to having non-goroutine-safe objects inside itself.
|
||||
|
||||
- a thread(state)
|
||||
- a function
|
||||
- an userdata
|
||||
- a table with a metatable
|
||||
|
||||
You **must not** send these objects from Go APIs to channels.
|
||||
|
||||
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
func receiver(ch, quit chan lua.LValue) {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
L.SetGlobal("ch", lua.LChannel(ch))
|
||||
L.SetGlobal("quit", lua.LChannel(quit))
|
||||
if err := L.DoString(`
|
||||
local exit = false
|
||||
while not exit do
|
||||
channel.select(
|
||||
{"|<-", ch, function(ok, v)
|
||||
if not ok then
|
||||
print("channel closed")
|
||||
exit = true
|
||||
else
|
||||
print("received:", v)
|
||||
end
|
||||
end},
|
||||
{"|<-", quit, function(ok, v)
|
||||
print("quit")
|
||||
exit = true
|
||||
end}
|
||||
)
|
||||
end
|
||||
`); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func sender(ch, quit chan lua.LValue) {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
L.SetGlobal("ch", lua.LChannel(ch))
|
||||
L.SetGlobal("quit", lua.LChannel(quit))
|
||||
if err := L.DoString(`
|
||||
ch:send("1")
|
||||
ch:send("2")
|
||||
`); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ch <- lua.LString("3")
|
||||
quit <- lua.LTrue
|
||||
}
|
||||
|
||||
func main() {
|
||||
ch := make(chan lua.LValue)
|
||||
quit := make(chan lua.LValue)
|
||||
go receiver(ch, quit)
|
||||
go sender(ch, quit)
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
|
||||
'''''''''''''''
|
||||
Go API
|
||||
'''''''''''''''
|
||||
|
||||
``ToChannel``, ``CheckChannel``, ``OptChannel`` are available.
|
||||
|
||||
Refer to `Go doc(LState methods) <http://godoc.org/github.com/yuin/gopher-lua>`_ for further information.
|
||||
|
||||
'''''''''''''''
|
||||
Lua API
|
||||
'''''''''''''''
|
||||
|
||||
- **channel.make([buf:int]) -> ch:channel**
|
||||
- Create new channel that has a buffer size of ``buf``. By default, ``buf`` is 0.
|
||||
|
||||
- **channel.select(case:table [, case:table, case:table ...]) -> {index:int, recv:any, ok}**
|
||||
- Same as the ``select`` statement in Go. It returns the index of the chosen case and, if that
|
||||
case was a receive operation, the value received and a boolean indicating whether the channel has been closed.
|
||||
- ``case`` is a table that outlined below.
|
||||
- receiving: `{"|<-", ch:channel [, handler:func(ok, data:any)]}`
|
||||
- sending: `{"<-|", ch:channel, data:any [, handler:func(data:any)]}`
|
||||
- default: `{"default" [, handler:func()]}`
|
||||
|
||||
``channel.select`` examples:
|
||||
|
||||
.. code-block:: lua
|
||||
|
||||
local idx, recv, ok = channel.select(
|
||||
{"|<-", ch1},
|
||||
{"|<-", ch2}
|
||||
)
|
||||
if not ok then
|
||||
print("closed")
|
||||
elseif idx == 1 then -- received from ch1
|
||||
print(recv)
|
||||
elseif idx == 2 then -- received from ch2
|
||||
print(recv)
|
||||
end
|
||||
|
||||
.. code-block:: lua
|
||||
|
||||
channel.select(
|
||||
{"|<-", ch1, function(ok, data)
|
||||
print(ok, data)
|
||||
end},
|
||||
{"<-|", ch2, "value", function(data)
|
||||
print(data)
|
||||
end},
|
||||
{"default", function()
|
||||
print("default action")
|
||||
end}
|
||||
)
|
||||
|
||||
- **channel:send(data:any)**
|
||||
- Send ``data`` over the channel.
|
||||
- **channel:receive() -> ok:bool, data:any**
|
||||
- Receive some data over the channel.
|
||||
- **channel:close()**
|
||||
- Close the channel.
|
||||
|
||||
''''''''''''''''''''''''''''''
|
||||
The LState pool pattern
|
||||
''''''''''''''''''''''''''''''
|
||||
To create per-thread LState instances, You can use the ``sync.Pool`` like mechanism.
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
type lStatePool struct {
|
||||
m sync.Mutex
|
||||
saved []*lua.LState
|
||||
}
|
||||
|
||||
func (pl *lStatePool) Get() *lua.LState {
|
||||
pl.m.Lock()
|
||||
defer pl.m.Unlock()
|
||||
n := len(pl.saved)
|
||||
if n == 0 {
|
||||
return pl.New()
|
||||
}
|
||||
x := pl.saved[n-1]
|
||||
pl.saved = pl.saved[0 : n-1]
|
||||
return x
|
||||
}
|
||||
|
||||
func (pl *lStatePool) New() *lua.LState {
|
||||
L := lua.NewState()
|
||||
// setting the L up here.
|
||||
// load scripts, set global variables, share channels, etc...
|
||||
return L
|
||||
}
|
||||
|
||||
func (pl *lStatePool) Put(L *lua.LState) {
|
||||
pl.m.Lock()
|
||||
defer pl.m.Unlock()
|
||||
pl.saved = append(pl.saved, L)
|
||||
}
|
||||
|
||||
func (pl *lStatePool) Shutdown() {
|
||||
for _, L := range pl.saved {
|
||||
L.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Global LState pool
|
||||
var luaPool = &lStatePool{
|
||||
saved: make([]*lua.LState, 0, 4),
|
||||
}
|
||||
|
||||
Now, you can get per-thread LState objects from the ``luaPool`` .
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
func MyWorker() {
|
||||
L := luaPool.Get()
|
||||
defer luaPool.Put(L)
|
||||
/* your code here */
|
||||
}
|
||||
|
||||
func main() {
|
||||
defer luaPool.Shutdown()
|
||||
go MyWorker()
|
||||
go MyWorker()
|
||||
/* etc... */
|
||||
}
|
||||
|
||||
|
||||
----------------------------------------------------------------
|
||||
Differences between Lua and GopherLua
|
||||
----------------------------------------------------------------
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Goroutines
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- GopherLua supports channel operations.
|
||||
- GopherLua has a type named ``channel``.
|
||||
- The ``channel`` table provides functions for performing channel operations.
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Unsupported functions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- ``string.dump``
|
||||
- ``os.setlocale``
|
||||
- ``collectgarbage``
|
||||
- ``lua_Debug.namewhat``
|
||||
- ``package.loadlib``
|
||||
- debug hooks
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Miscellaneous notes
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- ``file:setvbuf`` does not support a line buffering.
|
||||
- Daylight saving time is not supported.
|
||||
- GopherLua has a function to set an environment variable : ``os.setenv(name, value)``
|
||||
|
||||
----------------------------------------------------------------
|
||||
Standalone interpreter
|
||||
----------------------------------------------------------------
|
||||
Lua has an interpreter called ``lua`` . GopherLua has an interpreter called ``glua`` .
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
go get github.com/yuin/gopher-lua/cmd/glua
|
||||
|
||||
``glua`` has same options as ``lua`` .
|
||||
|
||||
----------------------------------------------------------------
|
||||
How to Contribute
|
||||
----------------------------------------------------------------
|
||||
Any kind of contributions are welcome.
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Pull requests
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Our workflow is based on the `github-flow <https://guides.github.com/introduction/flow/>`_ .
|
||||
|
||||
1. Create a new issue.
|
||||
2. Fork the project.
|
||||
3. Clone your fork and add the upstream.
|
||||
::
|
||||
|
||||
git remote add upstream https://github.com/yuin/gopher-lua.git
|
||||
|
||||
4. Pull new changes from the upstream.
|
||||
::
|
||||
|
||||
git checkout master
|
||||
git fetch upstream
|
||||
git merge upstream/master
|
||||
|
||||
5. Create a feature branch
|
||||
::
|
||||
|
||||
git checkout -b <branch-name>
|
||||
|
||||
6. Commit your changes and reference the issue number in your comment.
|
||||
::
|
||||
|
||||
git commit -m "Issue #<issue-ref> : <your message>"
|
||||
|
||||
7. Push the feature branch to your remote repository.
|
||||
::
|
||||
|
||||
git push origin <branch-name>
|
||||
|
||||
8. Open new pull request.
|
||||
|
||||
----------------------------------------------------------------
|
||||
Libraries for GopherLua
|
||||
----------------------------------------------------------------
|
||||
|
||||
- `gopher-luar <https://github.com/layeh/gopher-luar>`_ : Custom type reflection for gopher-lua
|
||||
- `gluamapper <https://github.com/yuin/gluamapper>`_ : Mapping a Lua table to a Go struct
|
||||
- `gluahttp <https://github.com/cjoudrey/gluahttp>`_ : HTTP request module for gopher-lua
|
||||
- `gopher-json <https://github.com/layeh/gopher-json>`_ : a simple JSON encoder/decoder for gopher-lua
|
||||
|
||||
----------------------------------------------------------------
|
||||
License
|
||||
----------------------------------------------------------------
|
||||
MIT
|
||||
|
||||
----------------------------------------------------------------
|
||||
Author
|
||||
----------------------------------------------------------------
|
||||
Yusuke Inuzuka
|
|
@ -0,0 +1,48 @@
|
|||
local ok, msg = pcall(function()
|
||||
dofile("notexist")
|
||||
end)
|
||||
assert(not ok and string.find(msg, ".*notexist.*"))
|
||||
|
||||
local ok, msg = pcall(function()
|
||||
assert(getfenv(2) == _G)
|
||||
end)
|
||||
assert(ok)
|
||||
|
||||
local i = 1
|
||||
local fn = assert(load(function()
|
||||
local tbl = {"return ", "1", "+", "1"}
|
||||
local v = tbl[i]
|
||||
i = i + 1
|
||||
return v
|
||||
end))
|
||||
assert(fn() == 2)
|
||||
|
||||
local fn, msg = load(function()
|
||||
return {}
|
||||
end)
|
||||
assert(not fn and string.find(msg, "must return a string"))
|
||||
|
||||
local i = 1
|
||||
local fn, msg = load(function()
|
||||
if i == 1 then
|
||||
i = i + 1
|
||||
return "returna"
|
||||
end
|
||||
end)
|
||||
assert(not fn and string.find(string.lower(msg), "eof"))
|
||||
|
||||
local ok, a, b = xpcall(function()
|
||||
return "a", "b"
|
||||
end,
|
||||
function(err)
|
||||
assert(nil)
|
||||
end)
|
||||
assert(ok and a == "a" and b == "b")
|
||||
|
||||
local ok, a, b = xpcall(function()
|
||||
error("error!")
|
||||
end,
|
||||
function(err)
|
||||
return err .. "!", "b"
|
||||
end)
|
||||
assert(not ok and string.find(a, "error!!") and b == nil)
|
|
@ -0,0 +1,17 @@
|
|||
co = coroutine.wrap(function()
|
||||
co()
|
||||
end)
|
||||
|
||||
local ok, msg = pcall(function()
|
||||
co()
|
||||
end)
|
||||
assert(not ok and string.find(msg, "can not resume a running thread"))
|
||||
|
||||
co = coroutine.wrap(function()
|
||||
return 1
|
||||
end)
|
||||
assert(co() == 1)
|
||||
local ok, msg = pcall(function()
|
||||
co()
|
||||
end)
|
||||
assert(not ok and string.find(msg, "can not resume a dead thread"))
|
|
@ -0,0 +1,83 @@
|
|||
-- debug lib tests
|
||||
-- debug stuff are partially implemented; hooks are not supported.
|
||||
|
||||
local function f1()
|
||||
end
|
||||
local env = {}
|
||||
local mt = {}
|
||||
debug.setfenv(f1, env)
|
||||
assert(debug.getfenv(f1) == env)
|
||||
debug.setmetatable(f1, mt)
|
||||
assert(debug.getmetatable(f1) == mt)
|
||||
|
||||
local function f2()
|
||||
local info = debug.getinfo(1, "Slunf")
|
||||
assert(info.currentline == 14)
|
||||
assert(info.linedefined == 13)
|
||||
assert(info.func == f2)
|
||||
assert(info.lastlinedefined == 25)
|
||||
assert(info.nups == 1)
|
||||
assert(info.name == "f2")
|
||||
assert(info.what == "Lua")
|
||||
if string.find(_VERSION, "GopherLua") then
|
||||
assert(info.source == "db.lua")
|
||||
end
|
||||
end
|
||||
f2()
|
||||
|
||||
local function f3()
|
||||
end
|
||||
local info = debug.getinfo(f3)
|
||||
assert(info.currentline == -1)
|
||||
assert(info.linedefined == 28)
|
||||
assert(info.func == f3)
|
||||
assert(info.lastlinedefined == 29)
|
||||
assert(info.nups == 0)
|
||||
assert(info.name == nil)
|
||||
assert(info.what == "Lua")
|
||||
if string.find(_VERSION, "GopherLua") then
|
||||
assert(info.source == "db.lua")
|
||||
end
|
||||
|
||||
local function f4()
|
||||
local a,b,c = 1,2,3
|
||||
local function f5()
|
||||
local name, value = debug.getlocal(2, 2)
|
||||
assert(debug.getlocal(2, 10) == nil)
|
||||
assert(name == "b")
|
||||
assert(value == 2)
|
||||
name = debug.setlocal(2, 2, 10)
|
||||
assert(debug.setlocal(2, 10, 10) == nil)
|
||||
assert(name == "b")
|
||||
|
||||
local d = a
|
||||
local e = c
|
||||
|
||||
local tb = debug.traceback("--msg--")
|
||||
assert(string.find(tb, "\\-\\-msg\\-\\-"))
|
||||
assert(string.find(tb, "in.*f5"))
|
||||
assert(string.find(tb, "in.*f4"))
|
||||
end
|
||||
f5()
|
||||
local name, value = debug.getupvalue(f5, 1)
|
||||
assert(debug.getupvalue(f5, 10) == nil)
|
||||
assert(name == "a")
|
||||
assert(value == 1)
|
||||
name = debug.setupvalue(f5, 1, 11)
|
||||
assert(debug.setupvalue(f5, 10, 11) == nil)
|
||||
assert(name == "a")
|
||||
assert(a == 11)
|
||||
|
||||
assert(b == 10) -- changed by debug.setlocal in f4
|
||||
end
|
||||
f4()
|
||||
|
||||
local ok, msg = pcall(function()
|
||||
debug.getlocal(10, 1)
|
||||
end)
|
||||
assert(not ok and string.find(msg, "level out of range"))
|
||||
|
||||
local ok, msg = pcall(function()
|
||||
debug.setlocal(10, 1, 1)
|
||||
end)
|
||||
assert(not ok and string.find(msg, "level out of range"))
|
|
@ -0,0 +1,68 @@
|
|||
|
||||
-- issue #10
|
||||
local function inspect(options)
|
||||
options = options or {}
|
||||
return type(options)
|
||||
end
|
||||
assert(inspect(nil) == "table")
|
||||
|
||||
local function inspect(options)
|
||||
options = options or setmetatable({}, {__mode = "test"})
|
||||
return type(options)
|
||||
end
|
||||
assert(inspect(nil) == "table")
|
||||
|
||||
-- issue #16
|
||||
local ok, msg = pcall(function()
|
||||
local a = {}
|
||||
a[nil] = 1
|
||||
end)
|
||||
assert(not ok and string.find(msg, "table index is nil", 1, true))
|
||||
|
||||
-- issue #19
|
||||
local tbl = {1,2,3,4,5}
|
||||
assert(#tbl == 5)
|
||||
assert(table.remove(tbl) == 5)
|
||||
assert(#tbl == 4)
|
||||
assert(table.remove(tbl, 3) == 3)
|
||||
assert(#tbl == 3)
|
||||
|
||||
-- issue #24
|
||||
local tbl = {string.find('hello.world', '.', 0)}
|
||||
assert(tbl[1] == 1 and tbl[2] == 1)
|
||||
assert(string.sub('hello.world', 0, 2) == "he")
|
||||
|
||||
-- issue 33
|
||||
local a,b
|
||||
a = function ()
|
||||
pcall(function()
|
||||
end)
|
||||
coroutine.yield("a")
|
||||
return b()
|
||||
end
|
||||
|
||||
b = function ()
|
||||
return "b"
|
||||
end
|
||||
|
||||
local co = coroutine.create(a)
|
||||
assert(select(2, coroutine.resume(co)) == "a")
|
||||
assert(select(2, coroutine.resume(co)) == "b")
|
||||
assert(coroutine.status(co) == "dead")
|
||||
|
||||
-- issue 37
|
||||
function test(a, b, c)
|
||||
b = b or string.format("b%s", a)
|
||||
c = c or string.format("c%s", a)
|
||||
assert(a == "test")
|
||||
assert(b == "btest")
|
||||
assert(c == "ctest")
|
||||
end
|
||||
test("test")
|
||||
|
||||
-- issue 39
|
||||
assert(string.match("あいうえお", ".*あ.*") == "あいうえお")
|
||||
assert(string.match("あいうえお", "あいうえお") == "あいうえお")
|
||||
|
||||
-- issue 47
|
||||
assert(string.gsub("A\nA", ".", "A") == "AAA")
|
|
@ -0,0 +1,18 @@
|
|||
local osname = "linux"
|
||||
if string.find(os.getenv("OS") or "", "Windows") then
|
||||
osname = "windows"
|
||||
end
|
||||
|
||||
if osname == "linux" then
|
||||
-- travis ci failed to start date command?
|
||||
-- assert(os.execute("date") == 0)
|
||||
assert(os.execute("date -a") == 1)
|
||||
else
|
||||
assert(os.execute("date /T") == 0)
|
||||
assert(os.execute("md") == 1)
|
||||
end
|
||||
|
||||
assert(os.getenv("PATH") ~= "")
|
||||
assert(os.getenv("_____GLUATEST______") == nil)
|
||||
assert(os.setenv("_____GLUATEST______", "1"))
|
||||
assert(os.getenv("_____GLUATEST______") == "1")
|
|
@ -0,0 +1,12 @@
|
|||
local a = {}
|
||||
assert(table.maxn(a) == 0)
|
||||
a["key"] = 1
|
||||
assert(table.maxn(a) == 0)
|
||||
table.insert(a, 10)
|
||||
table.insert(a, 3, 10)
|
||||
assert(table.maxn(a) == 3)
|
||||
|
||||
local ok, msg = pcall(function()
|
||||
table.insert(a)
|
||||
end)
|
||||
assert(not ok and string.find(msg, "wrong number of arguments"))
|
|
@ -0,0 +1,82 @@
|
|||
for i, v in ipairs({"hoge", {}, function() end, true, nil}) do
|
||||
local ok, msg = pcall(function()
|
||||
print(-v)
|
||||
end)
|
||||
assert(not ok and string.find(msg, "__unm undefined"))
|
||||
end
|
||||
|
||||
assert(#"abc" == 3)
|
||||
local tbl = {1,2,3}
|
||||
setmetatable(tbl, {__len = function(self)
|
||||
return 10
|
||||
end})
|
||||
assert(#tbl == 10)
|
||||
|
||||
setmetatable(tbl, nil)
|
||||
assert(#tbl == 3)
|
||||
|
||||
local ok, msg = pcall(function()
|
||||
return 1 < "hoge"
|
||||
end)
|
||||
assert(not ok and string.find(msg, "attempt to compare number with string"))
|
||||
|
||||
local ok, msg = pcall(function()
|
||||
return {} < (function() end)
|
||||
end)
|
||||
assert(not ok and string.find(msg, "attempt to compare table with function"))
|
||||
|
||||
local ok, msg = pcall(function()
|
||||
for n = nil,1 do
|
||||
print(1)
|
||||
end
|
||||
end)
|
||||
assert(not ok and string.find(msg, "for statement init must be a number"))
|
||||
|
||||
local ok, msg = pcall(function()
|
||||
for n = 1,nil do
|
||||
print(1)
|
||||
end
|
||||
end)
|
||||
assert(not ok and string.find(msg, "for statement limit must be a number"))
|
||||
|
||||
local ok, msg = pcall(function()
|
||||
for n = 1,10,nil do
|
||||
print(1)
|
||||
end
|
||||
end)
|
||||
assert(not ok and string.find(msg, "for statement step must be a number"))
|
||||
|
||||
local ok, msg = pcall(function()
|
||||
return {} + (function() end)
|
||||
end)
|
||||
assert(not ok and string.find(msg, "cannot perform add operation between table and function"))
|
||||
|
||||
local ok, msg = pcall(function()
|
||||
return {} .. (function() end)
|
||||
end)
|
||||
assert(not ok and string.find(msg, "cannot perform concat operation between table and function"))
|
||||
|
||||
-- test table with initial elements over 511
|
||||
local bigtable = {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,10}
|
||||
assert(bigtable[601] == 10)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue