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