Add network incentivisation service (#1452)
This commit is contained in:
parent
1a4fe50971
commit
354e6981ba
|
@ -766,6 +766,14 @@
|
|||
pruneopts = "NUT"
|
||||
revision = "ed27b6fd65218132ee50cd95f38474a3d8a2cd12"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:df2ffac3fa01a4eff2983f6993e3e56db39101a2c030f6bbfc99209b8a15ca9b"
|
||||
name = "github.com/russolsen/transit"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "0794b4c4505af6b9382d56e296a6796be0cf4d76"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:8737b20e873c7cad069dfafd6382659b4cdf9e80359cdc159b8ec464893c932e"
|
||||
name = "github.com/satori/go.uuid"
|
||||
|
@ -773,6 +781,14 @@
|
|||
pruneopts = "NUT"
|
||||
revision = "36e9d2ebbde5e3f13ab2e25625fd453271d6522e"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:11afe1ad32273aeda2ff5e80317638da5006aff881263b995038900a89494eff"
|
||||
name = "github.com/shopspring/decimal"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "cd690d0c9e2447b1ef2a129a6b7b49077da89b8e"
|
||||
version = "1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:96ea40e8fab1400b769ff9669dd7f7de613c3a1943dd7f451c4ccbb05115bcaa"
|
||||
name = "github.com/spaolacci/murmur3"
|
||||
|
@ -1100,6 +1116,7 @@
|
|||
"github.com/multiformats/go-multiaddr",
|
||||
"github.com/mutecomm/go-sqlcipher",
|
||||
"github.com/pborman/uuid",
|
||||
"github.com/russolsen/transit",
|
||||
"github.com/status-im/doubleratchet",
|
||||
"github.com/status-im/go-ethereum/common/hexutil",
|
||||
"github.com/status-im/migrate",
|
||||
|
|
File diff suppressed because one or more lines are too long
53
node/node.go
53
node/node.go
|
@ -1,6 +1,7 @@
|
|||
package node
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -8,10 +9,12 @@ import (
|
|||
"path/filepath"
|
||||
"time"
|
||||
|
||||
gethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"github.com/ethereum/go-ethereum/eth/downloader"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/ethereum/go-ethereum/les"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
|
@ -21,6 +24,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/p2p/nat"
|
||||
"github.com/status-im/status-go/mailserver"
|
||||
"github.com/status-im/status-go/params"
|
||||
"github.com/status-im/status-go/services/incentivisation"
|
||||
"github.com/status-im/status-go/services/peer"
|
||||
"github.com/status-im/status-go/services/personal"
|
||||
"github.com/status-im/status-go/services/shhext"
|
||||
|
@ -40,6 +44,7 @@ var (
|
|||
ErrPersonalServiceRegistrationFailure = errors.New("failed to register the personal api service")
|
||||
ErrStatusServiceRegistrationFailure = errors.New("failed to register the Status service")
|
||||
ErrPeerServiceRegistrationFailure = errors.New("failed to register the Peer service")
|
||||
ErrIncentivisationServiceRegistrationFailure = errors.New("failed to register the Incentivisation service")
|
||||
)
|
||||
|
||||
// All general log messages in this package should be routed through this logger.
|
||||
|
@ -98,6 +103,11 @@ func MakeNode(config *params.NodeConfig, db *leveldb.DB) (*node.Node, error) {
|
|||
return nil, fmt.Errorf("%v: %v", ErrWhisperServiceRegistrationFailure, err)
|
||||
}
|
||||
|
||||
// start incentivisation service
|
||||
if err := activateIncentivisationService(stack, config); err != nil {
|
||||
return nil, fmt.Errorf("%v: %v", ErrIncentivisationServiceRegistrationFailure, err)
|
||||
}
|
||||
|
||||
// start status service.
|
||||
if err := activateStatusService(stack, config); err != nil {
|
||||
return nil, fmt.Errorf("%v: %v", ErrStatusServiceRegistrationFailure, err)
|
||||
|
@ -333,6 +343,49 @@ func activateShhService(stack *node.Node, config *params.NodeConfig, db *leveldb
|
|||
})
|
||||
}
|
||||
|
||||
// activateIncentivisationService configures Whisper and adds it to the given node.
|
||||
func activateIncentivisationService(stack *node.Node, config *params.NodeConfig) (err error) {
|
||||
if !config.WhisperConfig.Enabled {
|
||||
logger.Info("SHH protocol is disabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
if !config.IncentivisationConfig.Enabled {
|
||||
logger.Info("Incentivisation is disabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Info("activating incentivisation")
|
||||
// TODO(dshulyak) add a config option to enable it by default, but disable if app is started from statusd
|
||||
return stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
|
||||
var w *whisper.Whisper
|
||||
if err := ctx.Service(&w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
incentivisationConfig := &incentivisation.ServiceConfig{
|
||||
ContractAddress: config.IncentivisationConfig.ContractAddress,
|
||||
RPCEndpoint: config.IncentivisationConfig.RPCEndpoint,
|
||||
IP: config.IncentivisationConfig.IP,
|
||||
Port: config.IncentivisationConfig.Port,
|
||||
}
|
||||
privateKey, err := crypto.HexToECDSA(config.NodeKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err := ethclient.DialContext(context.TODO(), incentivisationConfig.RPCEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contract, err := incentivisation.NewContract(gethcommon.HexToAddress(incentivisationConfig.ContractAddress), client, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return incentivisation.New(privateKey, whisper.NewPublicWhisperAPI(w), incentivisationConfig, contract), nil
|
||||
})
|
||||
}
|
||||
|
||||
// parseNodes creates list of enode.Node out of enode strings.
|
||||
func parseNodes(enodes []string) []*enode.Node {
|
||||
var nodes []*enode.Node
|
||||
|
|
|
@ -93,6 +93,34 @@ func (c *WhisperConfig) String() string {
|
|||
return string(data)
|
||||
}
|
||||
|
||||
// IncentivisationConfig holds incentivisation-related configuration
|
||||
type IncentivisationConfig struct {
|
||||
// Enabled flag specifies whether protocol is enabled
|
||||
Enabled bool `validate:"required"`
|
||||
// Endpoint for the RPC calls
|
||||
RPCEndpoint string `validate:"required"`
|
||||
// Contract address
|
||||
ContractAddress string `validate:"required"`
|
||||
// IP address that is used
|
||||
IP string `validate:"required"`
|
||||
// Port
|
||||
Port uint16 `validate:"required"`
|
||||
}
|
||||
|
||||
// String dumps config object as nicely indented JSON
|
||||
func (c *IncentivisationConfig) String() string {
|
||||
data, _ := json.MarshalIndent(c, "", " ") // nolint: gas
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// Validate validates the IncentivisationConfig struct and returns an error if inconsistent values are found
|
||||
func (c *IncentivisationConfig) Validate(validate *validator.Validate) error {
|
||||
if err := validate.Struct(c); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------
|
||||
// SwarmConfig
|
||||
// ----------
|
||||
|
@ -295,6 +323,9 @@ type NodeConfig struct {
|
|||
// WhisperConfig extra configuration for SHH
|
||||
WhisperConfig WhisperConfig `json:"WhisperConfig," validate:"structonly"`
|
||||
|
||||
// IncentivisationConfig extra configuration for incentivisation service
|
||||
IncentivisationConfig IncentivisationConfig `json:"IncentivisationConfig," validate:"structonly"`
|
||||
|
||||
// ShhextConfig keeps configuration for service running under shhext namespace.
|
||||
ShhextConfig ShhextConfig `json:"ShhextConfig," validate:"structonly"`
|
||||
|
||||
|
@ -631,6 +662,11 @@ func (c *NodeConfig) validateChildStructs(validate *validator.Validate) error {
|
|||
if err := c.ShhextConfig.Validate(validate); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.IncentivisationConfig.Enabled {
|
||||
if err := c.IncentivisationConfig.Validate(validate); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -45,6 +45,8 @@ func TestNewNodeConfigWithDefaults(t *testing.T) {
|
|||
// assert peers limits
|
||||
assert.Contains(t, c.RequireTopics, params.WhisperDiscv5Topic)
|
||||
assert.Contains(t, c.RequireTopics, discv5.Topic(params.LesTopic(int(c.NetworkID))))
|
||||
// assert incentivisation
|
||||
assert.Equal(t, false, c.IncentivisationConfig.Enabled)
|
||||
// assert other
|
||||
assert.Equal(t, false, c.HTTPEnabled)
|
||||
assert.Equal(t, false, c.IPCEnabled)
|
||||
|
@ -386,6 +388,103 @@ func TestNodeConfigValidate(t *testing.T) {
|
|||
}`,
|
||||
Error: "field BackupDisabledDataDir is required if PFSEnabled is true",
|
||||
},
|
||||
{
|
||||
Name: "Valid JSON config with incentivisation",
|
||||
Config: `{
|
||||
"NetworkId": 1,
|
||||
"DataDir": "/tmp/data",
|
||||
"BackupDisabledDataDir": "/tmp/data",
|
||||
"KeyStoreDir": "/tmp/data",
|
||||
"NoDiscovery": true,
|
||||
"IncentivisationConfig": {
|
||||
"Enabled": true,
|
||||
"IP": "127.0.0.1",
|
||||
"Port": 300,
|
||||
"RPCEndpoint": "http://test.com",
|
||||
"ContractAddress": "0xfffff"
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
Name: "Missing RPCEndpoint",
|
||||
Config: `{
|
||||
"NetworkId": 1,
|
||||
"DataDir": "/tmp/data",
|
||||
"BackupDisabledDataDir": "/tmp/data",
|
||||
"KeyStoreDir": "/tmp/data",
|
||||
"NoDiscovery": true,
|
||||
"IncentivisationConfig": {
|
||||
"Enabled": true,
|
||||
"IP": "127.0.0.1",
|
||||
"Port": 300,
|
||||
"ContractAddress": "0xfffff"
|
||||
}
|
||||
}`,
|
||||
FieldErrors: map[string]string{
|
||||
"RPCEndpoint": "required",
|
||||
},
|
||||
Error: "RPCEndpoint is required if incentivisation is enabled",
|
||||
},
|
||||
{
|
||||
Name: "Missing contract address",
|
||||
Config: `{
|
||||
"NetworkId": 1,
|
||||
"DataDir": "/tmp/data",
|
||||
"BackupDisabledDataDir": "/tmp/data",
|
||||
"KeyStoreDir": "/tmp/data",
|
||||
"NoDiscovery": true,
|
||||
"IncentivisationConfig": {
|
||||
"Enabled": true,
|
||||
"IP": "127.0.0.1",
|
||||
"Port": 300,
|
||||
"RPCEndpoint": "http://test.com"
|
||||
}
|
||||
}`,
|
||||
FieldErrors: map[string]string{
|
||||
"ContractAddress": "required",
|
||||
},
|
||||
Error: "field ContractAddress is required if incentivisation is enabled",
|
||||
},
|
||||
{
|
||||
Name: "Missing ip address",
|
||||
Config: `{
|
||||
"NetworkId": 1,
|
||||
"DataDir": "/tmp/data",
|
||||
"BackupDisabledDataDir": "/tmp/data",
|
||||
"KeyStoreDir": "/tmp/data",
|
||||
"NoDiscovery": true,
|
||||
"IncentivisationConfig": {
|
||||
"Enabled": true,
|
||||
"Port": 300,
|
||||
"RPCEndpoint": "http://test.com",
|
||||
"ContractAddress": "0xfffff"
|
||||
}
|
||||
}`,
|
||||
FieldErrors: map[string]string{
|
||||
"IP": "required",
|
||||
},
|
||||
Error: "field IP is required if incentivisation is enabled",
|
||||
},
|
||||
{
|
||||
Name: "Missing port",
|
||||
Config: `{
|
||||
"NetworkId": 1,
|
||||
"DataDir": "/tmp/data",
|
||||
"BackupDisabledDataDir": "/tmp/data",
|
||||
"KeyStoreDir": "/tmp/data",
|
||||
"NoDiscovery": true,
|
||||
"IncentivisationConfig": {
|
||||
"Enabled": true,
|
||||
"IP": "127.0.0.1",
|
||||
"RPCEndpoint": "http://test.com",
|
||||
"ContractAddress": "0xfffff"
|
||||
}
|
||||
}`,
|
||||
FieldErrors: map[string]string{
|
||||
"Port": "required",
|
||||
},
|
||||
Error: "field Port is required if incentivisation is enabled",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
@ -401,6 +500,7 @@ func TestNodeConfigValidate(t *testing.T) {
|
|||
if tc.Error == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
fmt.Println(tc.Error)
|
||||
require.Contains(t, err.Error(), tc.Error)
|
||||
}
|
||||
case nil:
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package incentivisation
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// PublicAPI represents a set of APIs from the `web3.peer` namespace.
|
||||
type PublicAPI struct {
|
||||
s *Service
|
||||
}
|
||||
|
||||
// NewAPI creates an instance of the peer API.
|
||||
func NewAPI(s *Service) *PublicAPI {
|
||||
return &PublicAPI{s: s}
|
||||
}
|
||||
|
||||
func (api *PublicAPI) Registered(context context.Context) error {
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package incentivisation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
gethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/status-im/status-go/mailserver/registry"
|
||||
"math/big"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Contract interface {
|
||||
Vote(opts *bind.TransactOpts, joinNodes []gethcommon.Address, removeNodes []gethcommon.Address) (*types.Transaction, error)
|
||||
GetCurrentSession(opts *bind.CallOpts) (*big.Int, error)
|
||||
Registered(opts *bind.CallOpts, publicKey []byte) (bool, error)
|
||||
RegisterNode(opts *bind.TransactOpts, publicKey []byte, ip uint32, port uint16) (*types.Transaction, error)
|
||||
ActiveNodeCount(opts *bind.CallOpts) (*big.Int, error)
|
||||
InactiveNodeCount(opts *bind.CallOpts) (*big.Int, error)
|
||||
GetNode(opts *bind.CallOpts, index *big.Int) ([]byte, uint32, uint16, uint32, uint32, error)
|
||||
GetInactiveNode(opts *bind.CallOpts, index *big.Int) ([]byte, uint32, uint16, uint32, uint32, error)
|
||||
VoteSync(opts *bind.TransactOpts, joinNodes []gethcommon.Address, removeNodes []gethcommon.Address) (*types.Transaction, error)
|
||||
}
|
||||
|
||||
type ContractImpl struct {
|
||||
registry.NodesV2
|
||||
client *ethclient.Client
|
||||
}
|
||||
|
||||
// VoteSync votes on the contract and wait until the transaction has been accepted, returns an error otherwise
|
||||
func (c *ContractImpl) VoteSync(opts *bind.TransactOpts, joinNodes []gethcommon.Address, removeNodes []gethcommon.Address) (*types.Transaction, error) {
|
||||
tx, err := c.Vote(opts, joinNodes, removeNodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for {
|
||||
receipt, _ := c.client.TransactionReceipt(context.TODO(), tx.Hash())
|
||||
if receipt != nil {
|
||||
if receipt.Status == 0 {
|
||||
return nil, errors.New("Invalid receipt")
|
||||
}
|
||||
break
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
return tx, nil
|
||||
|
||||
}
|
||||
|
||||
// NewContract creates a new instance of Contract, bound to a specific deployed contract.
|
||||
func NewContract(address gethcommon.Address, backend bind.ContractBackend, client *ethclient.Client) (Contract, error) {
|
||||
contract := &ContractImpl{}
|
||||
contract.client = client
|
||||
|
||||
caller, err := registry.NewNodesV2Caller(address, backend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
contract.NodesV2Caller = *caller
|
||||
|
||||
transactor, err := registry.NewNodesV2Transactor(address, backend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
contract.NodesV2Transactor = *transactor
|
||||
|
||||
filterer, err := registry.NewNodesV2Filterer(address, backend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
contract.NodesV2Filterer = *filterer
|
||||
|
||||
return contract, nil
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package incentivisation
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/russolsen/transit"
|
||||
)
|
||||
|
||||
type StatusMessageContent struct {
|
||||
ChatID string
|
||||
Text string
|
||||
}
|
||||
|
||||
type StatusMessage struct {
|
||||
Text string
|
||||
ContentT string
|
||||
MessageT string
|
||||
Clock int64
|
||||
Timestamp int64
|
||||
Content StatusMessageContent
|
||||
}
|
||||
|
||||
// CreateTextStatusMessage creates a StatusMessage.
|
||||
func CreateTextStatusMessage(text string, chatID string) StatusMessage {
|
||||
ts := time.Now().Unix() * 1000
|
||||
|
||||
return StatusMessage{
|
||||
Text: text,
|
||||
ContentT: "text/plain",
|
||||
MessageT: "public-group-user-message",
|
||||
Clock: ts * 100,
|
||||
Timestamp: ts,
|
||||
Content: StatusMessageContent{ChatID: chatID, Text: text},
|
||||
}
|
||||
}
|
||||
|
||||
func EncodeMessage(content string, chatID string) ([]byte, error) {
|
||||
value := CreateTextStatusMessage(content, chatID)
|
||||
var buf bytes.Buffer
|
||||
encoder := NewMessageEncoder(&buf)
|
||||
if err := encoder.Encode(value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// NewMessageEncoder returns a new Transit encoder
|
||||
// that can encode StatusMessage values.
|
||||
// More about Transit: https://github.com/cognitect/transit-format
|
||||
func NewMessageEncoder(w io.Writer) *transit.Encoder {
|
||||
encoder := transit.NewEncoder(w, false)
|
||||
encoder.AddHandler(statusMessageType, defaultStatusMessageValueEncoder)
|
||||
return encoder
|
||||
}
|
||||
|
||||
var (
|
||||
statusMessageType = reflect.TypeOf(StatusMessage{})
|
||||
defaultStatusMessageValueEncoder = &statusMessageValueEncoder{}
|
||||
)
|
||||
|
||||
type statusMessageValueEncoder struct{}
|
||||
|
||||
func (statusMessageValueEncoder) IsStringable(reflect.Value) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (statusMessageValueEncoder) Encode(e transit.Encoder, value reflect.Value, asString bool) error {
|
||||
message := value.Interface().(StatusMessage)
|
||||
taggedValue := transit.TaggedValue{
|
||||
Tag: "c4",
|
||||
Value: []interface{}{
|
||||
message.Text,
|
||||
message.ContentT,
|
||||
transit.Keyword(message.MessageT),
|
||||
message.Clock,
|
||||
message.Timestamp,
|
||||
map[interface{}]interface{}{
|
||||
transit.Keyword("chat-id"): message.Content.ChatID,
|
||||
transit.Keyword("text"): message.Content.Text,
|
||||
},
|
||||
},
|
||||
}
|
||||
return e.EncodeInterface(taggedValue, false)
|
||||
}
|
|
@ -0,0 +1,512 @@
|
|||
package incentivisation
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"crypto/ecdsa"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
gethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"math/big"
|
||||
"net"
|
||||
"sort"
|
||||
|
||||
whisper "github.com/status-im/whisper/whisperv6"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
gasLimit = 1001000
|
||||
pingIntervalAllowance = 240
|
||||
tickerInterval = 30
|
||||
defaultTopic = "status-incentivisation-topic"
|
||||
)
|
||||
|
||||
type Enode struct {
|
||||
PublicKey []byte
|
||||
IP net.IP
|
||||
Port uint16
|
||||
JoiningSession uint32
|
||||
ActiveSession uint32
|
||||
Active bool
|
||||
}
|
||||
|
||||
func formatEnodeURL(publicKey string, ip string, port uint16) string {
|
||||
return fmt.Sprintf("enode://%s:%s:%d", publicKey, ip, port)
|
||||
}
|
||||
|
||||
func (n *Enode) toEnodeURL() string {
|
||||
return formatEnodeURL(n.PublicKeyString(), n.IP.String(), n.Port)
|
||||
}
|
||||
|
||||
func (n *Enode) PublicKeyString() string {
|
||||
return hex.EncodeToString(n.PublicKey)
|
||||
}
|
||||
|
||||
type Whisper interface {
|
||||
Post(ctx context.Context, req whisper.NewMessage) (hexutil.Bytes, error)
|
||||
NewMessageFilter(req whisper.Criteria) (string, error)
|
||||
AddPrivateKey(ctx context.Context, privateKey hexutil.Bytes) (string, error)
|
||||
DeleteKeyPair(ctx context.Context, key string) (bool, error)
|
||||
GenerateSymKeyFromPassword(ctx context.Context, passwd string) (string, error)
|
||||
GetFilterMessages(id string) ([]*whisper.Message, error)
|
||||
}
|
||||
|
||||
type ServiceConfig struct {
|
||||
RPCEndpoint string
|
||||
ContractAddress string
|
||||
IP string
|
||||
Port uint16
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
w Whisper
|
||||
whisperKeyID string
|
||||
whisperSymKeyID string
|
||||
whisperFilterID string
|
||||
nodes map[string]*Enode
|
||||
ticker *time.Ticker
|
||||
quit chan struct{}
|
||||
config *ServiceConfig
|
||||
contract Contract
|
||||
privateKey *ecdsa.PrivateKey
|
||||
log log.Logger
|
||||
// The first round we will not be voting, as we might have incomplete data
|
||||
initialSession uint64
|
||||
// The current session
|
||||
currentSession uint64
|
||||
whisperPings map[string][]uint32
|
||||
}
|
||||
|
||||
// New returns a new incentivization Service
|
||||
func New(prv *ecdsa.PrivateKey, w Whisper, config *ServiceConfig, contract Contract) *Service {
|
||||
logger := log.New("package", "status-go/incentivisation/service")
|
||||
return &Service{
|
||||
w: w,
|
||||
config: config,
|
||||
privateKey: prv,
|
||||
log: logger,
|
||||
contract: contract,
|
||||
nodes: make(map[string]*Enode),
|
||||
whisperPings: make(map[string][]uint32),
|
||||
}
|
||||
}
|
||||
|
||||
// Protocols returns a new protocols list. In this case, there are none.
|
||||
func (s *Service) Protocols() []p2p.Protocol {
|
||||
return []p2p.Protocol{}
|
||||
}
|
||||
|
||||
// APIs returns a list of new APIs.
|
||||
func (s *Service) APIs() []rpc.API {
|
||||
apis := []rpc.API{
|
||||
{
|
||||
Namespace: "incentivisation",
|
||||
Version: "1.0",
|
||||
Service: NewAPI(s),
|
||||
Public: true,
|
||||
},
|
||||
}
|
||||
return apis
|
||||
}
|
||||
|
||||
// checkRegistered checks that a node is registered with the contract
|
||||
func (s *Service) checkRegistered() error {
|
||||
registered, err := s.registered()
|
||||
if err != nil {
|
||||
s.log.Error("error querying contract", "registered", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if registered {
|
||||
s.log.Debug("Already registered")
|
||||
return nil
|
||||
}
|
||||
_, err = s.register()
|
||||
if err != nil {
|
||||
s.log.Error("error querying contract", "registered", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureSession checks if we are in a new session and updates the session if so
|
||||
func (s *Service) ensureSession() (bool, error) {
|
||||
session, err := s.GetCurrentSession()
|
||||
if err != nil {
|
||||
s.log.Error("failed to get current session", "err", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
if session != s.currentSession {
|
||||
s.currentSession = session
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// checkPings checks we have received the expected pings since it was last called
|
||||
func (s *Service) checkPings() map[string]bool {
|
||||
result := make(map[string]bool)
|
||||
now := time.Now().Unix()
|
||||
s.log.Debug("checking votes", "votes", s.whisperPings)
|
||||
for enodeID, timestamps := range s.whisperPings {
|
||||
result[enodeID] = true
|
||||
|
||||
if len(timestamps) < 2 {
|
||||
s.log.Debug("Node failed check", "enodeID", enodeID)
|
||||
result[enodeID] = false
|
||||
continue
|
||||
}
|
||||
|
||||
sort.Slice(timestamps, func(i, j int) bool { return timestamps[i] < timestamps[j] })
|
||||
timestamps = append(timestamps, uint32(now))
|
||||
for i := 1; i < len(timestamps); i++ {
|
||||
|
||||
if timestamps[i]-timestamps[i-1] > pingIntervalAllowance {
|
||||
result[enodeID] = false
|
||||
}
|
||||
}
|
||||
if result[enodeID] {
|
||||
s.log.Debug("Node passed check", "enodeID", enodeID)
|
||||
} else {
|
||||
s.log.Debug("Node failed check", "enodeID", enodeID)
|
||||
}
|
||||
|
||||
}
|
||||
s.log.Debug("voting result", "result", result)
|
||||
return result
|
||||
}
|
||||
|
||||
// perform is the main loop, it posts a ping, registers with the contract, check the pings and votes
|
||||
func (s *Service) perform() error {
|
||||
hash, err := s.postPing()
|
||||
if err != nil {
|
||||
s.log.Error("Could not post ping", "err", err)
|
||||
return err
|
||||
}
|
||||
s.log.Debug("Posted ping", "hash", hash)
|
||||
|
||||
err = s.FetchEnodes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.fetchMessages()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.checkRegistered()
|
||||
if err != nil {
|
||||
s.log.Error("Could not check if node is registered with the contract", "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// This actually updates the session
|
||||
newSession, err := s.ensureSession()
|
||||
if err != nil {
|
||||
s.log.Error("Could not check session", "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !newSession {
|
||||
s.log.Debug("Not a new session idling")
|
||||
return nil
|
||||
}
|
||||
|
||||
result := s.checkPings()
|
||||
err = s.vote(result)
|
||||
if err != nil {
|
||||
s.log.Error("Could not vote", "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Reset whisper pings
|
||||
s.whisperPings = make(map[string][]uint32)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// vote reports to the contract the decisions of the votes
|
||||
func (s *Service) vote(result map[string]bool) error {
|
||||
var behavingNodes []gethcommon.Address
|
||||
var misbehavingNodes []gethcommon.Address
|
||||
auth := s.auth()
|
||||
|
||||
for enodeIDString, passedCheck := range result {
|
||||
enodeID, err := hex.DecodeString(enodeIDString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if passedCheck {
|
||||
behavingNodes = append(behavingNodes, publicKeyBytesToAddress(enodeID))
|
||||
} else {
|
||||
misbehavingNodes = append(misbehavingNodes, publicKeyBytesToAddress(enodeID))
|
||||
}
|
||||
}
|
||||
|
||||
_, err := s.contract.VoteSync(&bind.TransactOpts{
|
||||
GasLimit: gasLimit,
|
||||
From: auth.From,
|
||||
Signer: auth.Signer,
|
||||
}, behavingNodes, misbehavingNodes)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Service) startTicker() {
|
||||
s.ticker = time.NewTicker(tickerInterval * time.Second)
|
||||
s.quit = make(chan struct{})
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-s.ticker.C:
|
||||
err := s.perform()
|
||||
if err != nil {
|
||||
s.log.Error("could not execute tick", "err", err)
|
||||
}
|
||||
case <-s.quit:
|
||||
s.ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Service) Start(server *p2p.Server) error {
|
||||
s.log.Info("Incentivisation service started", "address", s.addressString(), "publickey", s.publicKeyString())
|
||||
s.startTicker()
|
||||
|
||||
session, err := s.GetCurrentSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.initialSession = session
|
||||
s.currentSession = session
|
||||
|
||||
whisperKeyID, err := s.w.AddPrivateKey(context.TODO(), crypto.FromECDSA(s.privateKey))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.whisperKeyID = whisperKeyID
|
||||
|
||||
whisperSymKeyID, err := s.w.GenerateSymKeyFromPassword(context.TODO(), defaultTopic)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.whisperSymKeyID = whisperSymKeyID
|
||||
|
||||
criteria := whisper.Criteria{
|
||||
SymKeyID: whisperSymKeyID,
|
||||
Topics: []whisper.TopicType{toWhisperTopic(defaultTopic)},
|
||||
}
|
||||
filterID, err := s.w.NewMessageFilter(criteria)
|
||||
if err != nil {
|
||||
s.log.Error("could not create filter", "err", err)
|
||||
return err
|
||||
}
|
||||
s.whisperFilterID = filterID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop is run when a service is stopped.
|
||||
func (s *Service) Stop() error {
|
||||
s.log.Info("Incentivisation service stopped")
|
||||
_, err := s.w.DeleteKeyPair(context.TODO(), s.whisperKeyID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Service) publicKeyBytes() []byte {
|
||||
return crypto.FromECDSAPub(&s.privateKey.PublicKey)[1:]
|
||||
}
|
||||
|
||||
func (s *Service) GetCurrentSession() (uint64, error) {
|
||||
response, err := s.contract.GetCurrentSession(nil)
|
||||
if err != nil {
|
||||
s.log.Error("failed to get current session", "err", err)
|
||||
return 0, err
|
||||
}
|
||||
return response.Uint64(), nil
|
||||
}
|
||||
|
||||
func (s *Service) registered() (bool, error) {
|
||||
response, err := s.contract.Registered(nil, s.publicKeyBytes())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *Service) register() (bool, error) {
|
||||
auth := s.auth()
|
||||
ip, err := ip2Long(s.config.IP)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
_, err = s.contract.RegisterNode(&bind.TransactOpts{
|
||||
GasLimit: gasLimit,
|
||||
From: auth.From,
|
||||
Signer: auth.Signer,
|
||||
}, s.publicKeyBytes(), ip, s.config.Port)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *Service) FetchEnodes() error {
|
||||
one := big.NewInt(1)
|
||||
|
||||
activeNodeCount, err := s.contract.ActiveNodeCount(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.log.Debug("fetched active node count", "count", activeNodeCount)
|
||||
for i := big.NewInt(0); i.Cmp(activeNodeCount) < 0; i.Add(i, one) {
|
||||
publicKey, ip, port, joiningSession, activeSession, err := s.contract.GetNode(nil, i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
node := &Enode{
|
||||
PublicKey: publicKey,
|
||||
IP: int2ip(ip),
|
||||
Port: port,
|
||||
JoiningSession: joiningSession,
|
||||
ActiveSession: activeSession,
|
||||
}
|
||||
|
||||
s.log.Debug("adding node", "node", node.toEnodeURL())
|
||||
if node.PublicKeyString() != s.publicKeyString() {
|
||||
s.nodes[node.PublicKeyString()] = node
|
||||
}
|
||||
}
|
||||
|
||||
inactiveNodeCount, err := s.contract.InactiveNodeCount(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.log.Debug("fetched inactive node count", "count", inactiveNodeCount)
|
||||
for i := big.NewInt(0); i.Cmp(inactiveNodeCount) < 0; i.Add(i, one) {
|
||||
publicKey, ip, port, joiningSession, activeSession, err := s.contract.GetInactiveNode(nil, i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
node := &Enode{
|
||||
PublicKey: publicKey,
|
||||
IP: int2ip(ip),
|
||||
Port: port,
|
||||
JoiningSession: joiningSession,
|
||||
ActiveSession: activeSession,
|
||||
}
|
||||
|
||||
s.log.Debug("adding node", "node", node.toEnodeURL())
|
||||
if node.PublicKeyString() != s.publicKeyString() {
|
||||
s.nodes[node.PublicKeyString()] = node
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (s *Service) publicKeyString() string {
|
||||
return hex.EncodeToString(s.publicKeyBytes())
|
||||
}
|
||||
|
||||
func (s *Service) addressString() string {
|
||||
buf := crypto.Keccak256Hash(s.publicKeyBytes())
|
||||
address := buf[12:]
|
||||
|
||||
return hex.EncodeToString(address)
|
||||
}
|
||||
|
||||
// postPing publishes a whisper message
|
||||
func (s *Service) postPing() (hexutil.Bytes, error) {
|
||||
msg := defaultWhisperMessage()
|
||||
|
||||
msg.Topic = toWhisperTopic(defaultTopic)
|
||||
|
||||
enodeURL := formatEnodeURL(s.publicKeyString(), s.config.IP, s.config.Port)
|
||||
payload, err := EncodeMessage(enodeURL, defaultTopic)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msg.Payload = payload
|
||||
msg.Sig = s.whisperKeyID
|
||||
msg.SymKeyID = s.whisperSymKeyID
|
||||
|
||||
return s.w.Post(context.TODO(), msg)
|
||||
}
|
||||
|
||||
// fetchMessages checks for whisper messages
|
||||
func (s *Service) fetchMessages() error {
|
||||
messages, err := s.w.GetFilterMessages(s.whisperFilterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := 0; i < len(messages); i++ {
|
||||
signature := hex.EncodeToString(messages[i].Sig[1:])
|
||||
timestamp := messages[i].Timestamp
|
||||
if s.nodes[signature] != nil {
|
||||
s.whisperPings[signature] = append(s.whisperPings[signature], timestamp)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) auth() *bind.TransactOpts {
|
||||
return bind.NewKeyedTransactor(s.privateKey)
|
||||
}
|
||||
|
||||
func ip2Long(ip string) (uint32, error) {
|
||||
var long uint32
|
||||
err := binary.Read(bytes.NewBuffer(net.ParseIP(ip).To4()), binary.BigEndian, &long)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return long, nil
|
||||
}
|
||||
|
||||
func toWhisperTopic(s string) whisper.TopicType {
|
||||
return whisper.BytesToTopic(crypto.Keccak256([]byte(s)))
|
||||
}
|
||||
|
||||
func defaultWhisperMessage() whisper.NewMessage {
|
||||
msg := whisper.NewMessage{}
|
||||
|
||||
msg.TTL = 10
|
||||
msg.PowTarget = 0.002
|
||||
msg.PowTime = 1
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
func int2ip(nn uint32) net.IP {
|
||||
ip := make(net.IP, 4)
|
||||
binary.BigEndian.PutUint32(ip, nn)
|
||||
return ip
|
||||
}
|
||||
|
||||
func publicKeyBytesToAddress(publicKey []byte) gethcommon.Address {
|
||||
buf := crypto.Keccak256Hash(publicKey)
|
||||
address := buf[12:]
|
||||
|
||||
return gethcommon.HexToAddress(hex.EncodeToString(address))
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
package incentivisation
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
gethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
whisper "github.com/status-im/whisper/whisperv6"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
var (
|
||||
nodeOne = []byte{0x01}
|
||||
nodeTwo = []byte{0x02}
|
||||
)
|
||||
|
||||
type MockContract struct {
|
||||
currentSession *big.Int
|
||||
activeNodes [][]byte
|
||||
inactiveNodes [][]byte
|
||||
votes []Vote
|
||||
}
|
||||
|
||||
type Vote struct {
|
||||
joinNodes []gethcommon.Address
|
||||
removeNodes []gethcommon.Address
|
||||
}
|
||||
|
||||
type MockWhisper struct {
|
||||
sentMessages []whisper.NewMessage
|
||||
filterMessages []*whisper.Message
|
||||
}
|
||||
|
||||
func BuildMockContract() *MockContract {
|
||||
contract := &MockContract{
|
||||
currentSession: big.NewInt(0),
|
||||
}
|
||||
|
||||
contract.activeNodes = append(contract.activeNodes, nodeOne)
|
||||
contract.inactiveNodes = append(contract.activeNodes, nodeTwo)
|
||||
return contract
|
||||
}
|
||||
|
||||
func (c *MockContract) Vote(opts *bind.TransactOpts, joinNodes []gethcommon.Address, removeNodes []gethcommon.Address) (*types.Transaction, error) {
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *MockContract) VoteSync(opts *bind.TransactOpts, joinNodes []gethcommon.Address, removeNodes []gethcommon.Address) (*types.Transaction, error) {
|
||||
c.votes = append(c.votes, Vote{
|
||||
joinNodes: joinNodes,
|
||||
removeNodes: removeNodes,
|
||||
})
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *MockContract) GetCurrentSession(opts *bind.CallOpts) (*big.Int, error) {
|
||||
return c.currentSession, nil
|
||||
}
|
||||
func (c *MockContract) Registered(opts *bind.CallOpts, publicKey []byte) (bool, error) {
|
||||
|
||||
for _, e := range c.activeNodes {
|
||||
if bytes.Equal(publicKey, e) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, e := range c.inactiveNodes {
|
||||
if bytes.Equal(publicKey, e) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (c *MockContract) RegisterNode(opts *bind.TransactOpts, publicKey []byte, ip uint32, port uint16) (*types.Transaction, error) {
|
||||
c.inactiveNodes = append(c.inactiveNodes, publicKey)
|
||||
return nil, nil
|
||||
}
|
||||
func (c *MockContract) ActiveNodeCount(opts *bind.CallOpts) (*big.Int, error) {
|
||||
return big.NewInt(int64(len(c.activeNodes))), nil
|
||||
}
|
||||
func (c *MockContract) InactiveNodeCount(opts *bind.CallOpts) (*big.Int, error) {
|
||||
return big.NewInt(int64(len(c.inactiveNodes))), nil
|
||||
}
|
||||
|
||||
func (c *MockContract) GetNode(opts *bind.CallOpts, index *big.Int) ([]byte, uint32, uint16, uint32, uint32, error) {
|
||||
return c.activeNodes[index.Int64()], 0, 0, 0, 0, nil
|
||||
}
|
||||
func (c *MockContract) GetInactiveNode(opts *bind.CallOpts, index *big.Int) ([]byte, uint32, uint16, uint32, uint32, error) {
|
||||
return c.inactiveNodes[index.Int64()], 0, 0, 0, 0, nil
|
||||
}
|
||||
|
||||
func (w *MockWhisper) Post(ctx context.Context, req whisper.NewMessage) (hexutil.Bytes, error) {
|
||||
w.sentMessages = append(w.sentMessages, req)
|
||||
return nil, nil
|
||||
}
|
||||
func (w *MockWhisper) NewMessageFilter(req whisper.Criteria) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
func (w *MockWhisper) AddPrivateKey(ctx context.Context, privateKey hexutil.Bytes) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
func (w *MockWhisper) DeleteKeyPair(ctx context.Context, key string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
func (w *MockWhisper) GenerateSymKeyFromPassword(ctx context.Context, passwd string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
func (w *MockWhisper) GetFilterMessages(id string) ([]*whisper.Message, error) {
|
||||
return w.filterMessages, nil
|
||||
}
|
||||
|
||||
func TestIncentivisationSuite(t *testing.T) {
|
||||
suite.Run(t, new(IncentivisationSuite))
|
||||
}
|
||||
|
||||
type IncentivisationSuite struct {
|
||||
suite.Suite
|
||||
service *Service
|
||||
mockWhisper *MockWhisper
|
||||
mockContract *MockContract
|
||||
}
|
||||
|
||||
func (s *IncentivisationSuite) SetupTest() {
|
||||
privateKey, err := crypto.GenerateKey()
|
||||
config := &ServiceConfig{
|
||||
IP: "192.168.1.1",
|
||||
}
|
||||
contract := BuildMockContract()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
w := &MockWhisper{}
|
||||
s.service = New(privateKey, w, config, contract)
|
||||
s.mockWhisper = w
|
||||
s.mockContract = contract
|
||||
}
|
||||
|
||||
func (s *IncentivisationSuite) TestStart() {
|
||||
err := s.service.Start(nil)
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
|
||||
func (s *IncentivisationSuite) TestPerform() {
|
||||
err := s.service.Start(nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = s.service.perform()
|
||||
s.Require().NoError(err)
|
||||
|
||||
// It registers with the contract if not registered
|
||||
registered, err := s.service.registered()
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal(true, registered)
|
||||
|
||||
now := time.Now().Unix()
|
||||
// Add some envelopes
|
||||
s.mockWhisper.filterMessages = []*whisper.Message{
|
||||
{
|
||||
// We strip the first byte when processing
|
||||
Sig: append(nodeOne, nodeOne[0]),
|
||||
Timestamp: uint32(now - pingIntervalAllowance),
|
||||
},
|
||||
{
|
||||
Sig: append(nodeOne, nodeOne[0]),
|
||||
Timestamp: uint32(now - (pingIntervalAllowance * 2)),
|
||||
},
|
||||
{
|
||||
Sig: append(nodeTwo, nodeTwo[0]),
|
||||
Timestamp: uint32(now - (pingIntervalAllowance * 2)),
|
||||
},
|
||||
}
|
||||
|
||||
// It publishes a ping on whisper
|
||||
s.Require().Equal(1, len(s.mockWhisper.sentMessages))
|
||||
|
||||
// It should not vote
|
||||
s.Require().Equal(0, len(s.mockContract.votes))
|
||||
|
||||
// We increase the session
|
||||
s.mockContract.currentSession = s.mockContract.currentSession.Add(s.mockContract.currentSession, big.NewInt(1))
|
||||
|
||||
// We perform again
|
||||
err = s.service.perform()
|
||||
s.Require().NoError(err)
|
||||
|
||||
// it should now vote
|
||||
s.Require().Equal(1, len(s.mockContract.votes))
|
||||
// Node one should have been voted up
|
||||
s.Require().Equal(1, len(s.mockContract.votes[0].joinNodes))
|
||||
s.Require().Equal(publicKeyBytesToAddress(nodeOne), s.mockContract.votes[0].joinNodes[0])
|
||||
// Node two should have been voted down
|
||||
s.Require().Equal(1, len(s.mockContract.votes[0].removeNodes))
|
||||
s.Require().Equal(publicKeyBytesToAddress(nodeTwo), s.mockContract.votes[0].removeNodes[0])
|
||||
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright 2016 Russ Olsen. All Rights Reserved.
|
||||
//
|
||||
// This code is a Go port of the Java version created and maintained by Cognitect, therefore:
|
||||
//
|
||||
// Copyright 2014 Cognitect. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package transit
|
||||
|
||||
type CMapEntry struct {
|
||||
Key interface{}
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
func NewCMapEntry(key, value interface{}) *CMapEntry {
|
||||
return &CMapEntry{Key: key, Value: value}
|
||||
}
|
||||
|
||||
// CMap is used to hold maps that have composite keys (i.e. #cmap values).
|
||||
// Since Go arrays and maps cannot be used as map keys, a CMap is represented
|
||||
// here as a simple array of key/value entrys.
|
||||
|
||||
type CMap struct {
|
||||
Entries []CMapEntry
|
||||
}
|
||||
|
||||
func NewCMap() *CMap {
|
||||
return &CMap{}
|
||||
}
|
||||
|
||||
// FindBy searches thru the map, calling mf on each key in turn
|
||||
// and returns the first entry for which mf evaluates to true.
|
||||
|
||||
func (cm CMap) FindBy(key interface{}, mf MatchF) *CMapEntry {
|
||||
for _, entry := range cm.Entries {
|
||||
if mf(key, entry.Key) {
|
||||
return &entry
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find a given key in the map and return it's corresponding value.
|
||||
// The search thru the keys is done with ==, so the keys in the map
|
||||
// must be comparable.
|
||||
|
||||
func (cm CMap) Index(keyValue interface{}) interface{} {
|
||||
entry := cm.FindBy(keyValue, Equals)
|
||||
|
||||
if entry == nil {
|
||||
return nil
|
||||
}
|
||||
return entry.Value
|
||||
}
|
||||
|
||||
func (cm CMap) Put(key, value interface{}, mf MatchF) *CMap {
|
||||
entry := cm.FindBy(key, mf)
|
||||
|
||||
if entry != nil {
|
||||
entry.Value = value
|
||||
} else {
|
||||
entry = NewCMapEntry(key, value)
|
||||
cm.Entries = append(cm.Entries, *entry)
|
||||
}
|
||||
return &cm
|
||||
}
|
||||
|
||||
// Append inserts a new key/value pair w/o paying attention to
|
||||
// duplicate keys.
|
||||
func (cm *CMap) Append(key, value interface{}) *CMap {
|
||||
entry := NewCMapEntry(key, value)
|
||||
cm.Entries = append(cm.Entries, *entry)
|
||||
return cm
|
||||
}
|
||||
|
||||
// Size returns the number of key/value pairs.
|
||||
func (cm *CMap) Size() int {
|
||||
return len(cm.Entries)
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2016 Russ Olsen. All Rights Reserved.
|
||||
//
|
||||
// This code is a Go port of the Java version created and maintained by Cognitect, therefore:
|
||||
//
|
||||
// Copyright 2014 Cognitect. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package transit
|
||||
|
||||
const start = "~"
|
||||
const reserved = "`"
|
||||
const sub = "^"
|
||||
|
||||
const escapeTag = "~~"
|
||||
const escapeSub = "~^"
|
||||
const escapeRes = "~`"
|
||||
|
||||
const startTag = "~#"
|
||||
const startSym = "~$"
|
||||
const startKW = "~:"
|
||||
|
||||
const mapAsArray = "^ "
|
|
@ -0,0 +1,322 @@
|
|||
// Copyright 2016 Russ Olsen. All Rights Reserved.
|
||||
//
|
||||
// This code is a Go port of the Java version created and maintained by Cognitect, therefore:
|
||||
//
|
||||
// Copyright 2014 Cognitect. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package transit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Handler func(Decoder, interface{}) (interface{}, error)
|
||||
|
||||
type Decoder struct {
|
||||
jsd *json.Decoder
|
||||
decoders map[string]Handler
|
||||
cache *RollingCache
|
||||
}
|
||||
|
||||
// NewDecoder returns a new Decoder, ready to read from r.
|
||||
func NewDecoder(r io.Reader) *Decoder {
|
||||
jsd := json.NewDecoder(r)
|
||||
return NewJsonDecoder(jsd)
|
||||
}
|
||||
|
||||
// NewDecoder returns a new Decoder, ready to read from jsr.
|
||||
func NewJsonDecoder(jsd *json.Decoder) *Decoder {
|
||||
jsd.UseNumber()
|
||||
|
||||
decoders := make(map[string]Handler)
|
||||
|
||||
d := Decoder{jsd: jsd, decoders: decoders, cache: NewRollingCache()}
|
||||
initHandlers(&d)
|
||||
|
||||
return &d
|
||||
}
|
||||
|
||||
func initHandlers(d *Decoder) {
|
||||
d.AddHandler("_", DecodeNil)
|
||||
d.AddHandler(":", DecodeKeyword)
|
||||
d.AddHandler("?", DecodeBoolean)
|
||||
d.AddHandler("b", DecodeByte)
|
||||
d.AddHandler("d", DecodeFloat)
|
||||
d.AddHandler("i", DecodeInteger)
|
||||
d.AddHandler("n", DecodeBigInteger)
|
||||
d.AddHandler("f", DecodeDecimal)
|
||||
d.AddHandler("c", DecodeRune)
|
||||
d.AddHandler("$", DecodeSymbol)
|
||||
d.AddHandler("t", DecodeRFC3339)
|
||||
d.AddHandler("m", DecodeTime)
|
||||
d.AddHandler("u", DecodeUUID)
|
||||
d.AddHandler("r", DecodeURI)
|
||||
d.AddHandler("'", DecodeQuote)
|
||||
d.AddHandler("z", DecodeSpecialNumber)
|
||||
|
||||
d.AddHandler("set", DecodeSet)
|
||||
d.AddHandler("link", DecodeLink)
|
||||
d.AddHandler("list", DecodeList)
|
||||
d.AddHandler("cmap", DecodeCMap)
|
||||
d.AddHandler("ratio", DecodeRatio)
|
||||
d.AddHandler("unknown", DecodeIdentity)
|
||||
|
||||
}
|
||||
|
||||
// AddHandler adds a new handler to the decoder, allowing you to extend the types it can handle.
|
||||
func (d Decoder) AddHandler(tag string, valueDecoder Handler) {
|
||||
d.decoders[tag] = valueDecoder
|
||||
}
|
||||
|
||||
func (d Decoder) parseString(s string) (interface{}, error) {
|
||||
|
||||
if d.cache.HasKey(s) {
|
||||
return d.Parse(d.cache.Read(s), false)
|
||||
|
||||
} else if !strings.HasPrefix(s, start) {
|
||||
return s, nil
|
||||
|
||||
} else if strings.HasPrefix(s, startTag) {
|
||||
return TagId(s[2:]), nil
|
||||
|
||||
} else if vd := d.decoders[s[1:2]]; vd != nil {
|
||||
return vd(d, s[2:])
|
||||
|
||||
} else if strings.HasPrefix(s, escapeTag) ||
|
||||
strings.HasPrefix(s, escapeSub) ||
|
||||
strings.HasPrefix(s, escapeRes) {
|
||||
return s[1:], nil
|
||||
|
||||
} else {
|
||||
tv := TaggedValue{TagId(s[1:2]), s[2:]}
|
||||
return d.decoders["unknown"](d, tv)
|
||||
}
|
||||
}
|
||||
|
||||
func (d Decoder) parseSingleEntryMap(m map[string]interface{}) (interface{}, error) {
|
||||
// The loop here is just a convenient way to get at the only
|
||||
// entry in the map.
|
||||
for k, v := range m {
|
||||
key, err := d.Parse(k, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
value, err := d.Parse(v, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tag, isTag := key.(TagId); isTag {
|
||||
tv := TaggedValue{Tag: tag, Value: value}
|
||||
valueDecoder := d.DecoderFor(tag)
|
||||
return valueDecoder(d, tv)
|
||||
} else {
|
||||
return map[interface{}]interface{}{key: value}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil // Should never get here
|
||||
}
|
||||
|
||||
func (d Decoder) parseMultiEntryMap(m map[string]interface{}) (interface{}, error) {
|
||||
var result = make(map[interface{}]interface{})
|
||||
|
||||
for k, v := range m {
|
||||
key, err := d.Parse(k, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
value, err := d.Parse(v, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result[key] = value
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (d Decoder) parseMap(m map[string]interface{}) (interface{}, error) {
|
||||
if len(m) != 1 {
|
||||
return d.parseMultiEntryMap(m)
|
||||
} else {
|
||||
return d.parseSingleEntryMap(m)
|
||||
}
|
||||
}
|
||||
|
||||
func (d Decoder) parseNormalArray(x []interface{}) (interface{}, error) {
|
||||
var result = make([]interface{}, len(x))
|
||||
|
||||
for i, v := range x {
|
||||
var err error
|
||||
result[i], err = d.Parse(v, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (d Decoder) parseCMap(x []interface{}) (interface{}, error) {
|
||||
var result = NewCMap()
|
||||
|
||||
l := len(x)
|
||||
|
||||
for i := 1; i < l; i += 2 {
|
||||
key, err := d.Parse(x[i], true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
value, err := d.Parse(x[i+1], false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Append(key, value)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (d Decoder) parseArrayMap(x []interface{}) (interface{}, error) {
|
||||
result := make(map[interface{}]interface{})
|
||||
|
||||
l := len(x)
|
||||
|
||||
for i := 1; i < l; i += 2 {
|
||||
key, err := d.Parse(x[i], true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
value, err := d.Parse(x[i+1], false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[key] = value
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (d Decoder) DecoderFor(tagid TagId) Handler {
|
||||
key := string(tagid)
|
||||
|
||||
handler := d.decoders[key]
|
||||
if handler == nil {
|
||||
handler = d.decoders["unknown"]
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
func (d Decoder) parseArray(x []interface{}) (interface{}, error) {
|
||||
if len(x) == 0 {
|
||||
return x, nil
|
||||
}
|
||||
|
||||
e0, err := d.Parse(x[0], false)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if e0 == mapAsArray {
|
||||
return d.parseArrayMap(x)
|
||||
}
|
||||
|
||||
if tagId, isTag := e0.(TagId); isTag {
|
||||
var value interface{}
|
||||
|
||||
if value, err = d.Parse(x[1], false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tv := TaggedValue{Tag: tagId, Value: value}
|
||||
valueDecoder := d.DecoderFor(tagId)
|
||||
return valueDecoder(d, tv)
|
||||
}
|
||||
|
||||
return d.parseNormalArray(x)
|
||||
}
|
||||
|
||||
func (d Decoder) parseNumber(x json.Number) (interface{}, error) {
|
||||
var s = x.String()
|
||||
var err error
|
||||
|
||||
var result interface{}
|
||||
|
||||
if strings.ContainsAny(s, ".Ee") {
|
||||
result, err = x.Float64()
|
||||
} else {
|
||||
result, err = x.Int64()
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (d Decoder) Parse(x interface{}, asKey bool) (interface{}, error) {
|
||||
|
||||
switch v := x.(type) {
|
||||
default:
|
||||
return nil, &TransitError{Message: "Unexpected type"}
|
||||
case nil:
|
||||
return v, nil
|
||||
|
||||
case bool:
|
||||
return v, nil
|
||||
|
||||
case json.Number:
|
||||
return d.parseNumber(v)
|
||||
|
||||
case string:
|
||||
result, err := d.parseString(v)
|
||||
|
||||
if err == nil && d.cache.IsCacheable(v, asKey) {
|
||||
d.cache.Write(v)
|
||||
}
|
||||
return result, err
|
||||
|
||||
case map[string]interface{}:
|
||||
return d.parseMap(v)
|
||||
|
||||
case []interface{}:
|
||||
return d.parseArray(v)
|
||||
}
|
||||
}
|
||||
|
||||
// Decode decodes the next Transit value from the stream.
|
||||
func (d Decoder) Decode() (interface{}, error) {
|
||||
var jsonObject interface{}
|
||||
var err = d.jsd.Decode(&jsonObject)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return d.Parse(jsonObject, false)
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeFromString is a handly function that decodes Transit data held in a string.
|
||||
func DecodeFromString(s string) (interface{}, error) {
|
||||
reader := strings.NewReader(s)
|
||||
decoder := NewDecoder(reader)
|
||||
return decoder.Decode()
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
// Copyright 2016 Russ Olsen. All Rights Reserved.
|
||||
//
|
||||
// This code is a Go port of the Java version created and maintained by Cognitect, therefore:
|
||||
//
|
||||
// Copyright 2014 Cognitect. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package transit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type DataEmitter interface {
|
||||
Emit(s string) error
|
||||
EmitString(s string, cacheable bool) error
|
||||
EmitTag(s string) error
|
||||
EmitInt(i int64, asKey bool) error
|
||||
EmitFloat(f float64, asKey bool) error
|
||||
EmitNil(asKey bool) error
|
||||
EmitBool(bool, asKey bool) error
|
||||
EmitStartArray() error
|
||||
EmitArraySeparator() error
|
||||
EmitEndArray() error
|
||||
|
||||
EmitStartMap() error
|
||||
EmitMapSeparator() error
|
||||
EmitKeySeparator() error
|
||||
EmitEndMap() error
|
||||
}
|
||||
|
||||
type JsonEmitter struct {
|
||||
writer io.Writer
|
||||
cache Cache
|
||||
}
|
||||
|
||||
func NewJsonEmitter(w io.Writer, cache Cache) *JsonEmitter {
|
||||
return &JsonEmitter{writer: w, cache: cache}
|
||||
}
|
||||
|
||||
// Emit the string unaltered and without quotes. This is the lowest level emitter.
|
||||
|
||||
func (je JsonEmitter) Emit(s string) error {
|
||||
_, err := je.writer.Write([]byte(s))
|
||||
return err
|
||||
}
|
||||
|
||||
// EmitBase emits the basic value supplied, encoding it as JSON.
|
||||
|
||||
func (je JsonEmitter) EmitBase(x interface{}) error {
|
||||
bytes, err := json.Marshal(x)
|
||||
if err == nil {
|
||||
_, err = je.writer.Write(bytes)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// EmitsTag emits a transit #tag. The string supplied should not include the '#'.
|
||||
|
||||
func (je JsonEmitter) EmitTag(s string) error {
|
||||
return je.EmitString("~#"+s, true)
|
||||
}
|
||||
|
||||
func (je JsonEmitter) EmitString(s string, cacheable bool) error {
|
||||
if je.cache.IsCacheable(s, cacheable) {
|
||||
s = je.cache.Write(s)
|
||||
}
|
||||
return je.EmitBase(s)
|
||||
}
|
||||
|
||||
const MaxJsonInt = 1<<53 - 1
|
||||
|
||||
func (je JsonEmitter) EmitInt(i int64, asKey bool) error {
|
||||
if asKey || (i > MaxJsonInt) {
|
||||
return je.EmitString(fmt.Sprintf("~i%d", i), asKey)
|
||||
}
|
||||
return je.EmitBase(i)
|
||||
}
|
||||
|
||||
func (je JsonEmitter) EmitNil(asKey bool) error {
|
||||
if asKey {
|
||||
return je.EmitString("~_", false)
|
||||
} else {
|
||||
return je.EmitBase(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (je JsonEmitter) EmitFloat(f float64, asKey bool) error {
|
||||
if asKey {
|
||||
return je.EmitString(fmt.Sprintf("~d%g", f), asKey)
|
||||
} else {
|
||||
s := fmt.Sprintf("%g", f)
|
||||
if !strings.ContainsAny(s, ".eE") {
|
||||
s = s + ".0" // Horible hack!
|
||||
}
|
||||
return je.Emit(s)
|
||||
}
|
||||
}
|
||||
|
||||
func (je JsonEmitter) EmitStartArray() error {
|
||||
return je.Emit("[")
|
||||
}
|
||||
|
||||
func (je JsonEmitter) EmitEndArray() error {
|
||||
return je.Emit("]")
|
||||
}
|
||||
|
||||
func (je JsonEmitter) EmitArraySeparator() error {
|
||||
return je.Emit(",")
|
||||
}
|
||||
|
||||
func (je JsonEmitter) EmitStartMap() error {
|
||||
return je.Emit("{")
|
||||
}
|
||||
|
||||
func (je JsonEmitter) EmitEndMap() error {
|
||||
return je.Emit("}")
|
||||
}
|
||||
|
||||
func (je JsonEmitter) EmitMapSeparator() error {
|
||||
return je.Emit(",")
|
||||
}
|
||||
|
||||
func (je JsonEmitter) EmitKeySeparator() error {
|
||||
return je.Emit(":")
|
||||
}
|
||||
|
||||
func (je JsonEmitter) EmitBool(x bool, asKey bool) error {
|
||||
if asKey {
|
||||
if x {
|
||||
return je.EmitString("~?t", false)
|
||||
} else {
|
||||
return je.EmitString("~?f", false)
|
||||
}
|
||||
} else {
|
||||
return je.EmitBase(x)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,212 @@
|
|||
// Copyright 2016 Russ Olsen. All Rights Reserved.
|
||||
//
|
||||
// This code is a Go port of the Java version created and maintained by Cognitect, therefore:
|
||||
//
|
||||
// Copyright 2014 Cognitect. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package transit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"container/list"
|
||||
"github.com/pborman/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"io"
|
||||
"math/big"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Encoder struct {
|
||||
emitter DataEmitter
|
||||
valueEncoders map[interface{}]ValueEncoder
|
||||
}
|
||||
|
||||
var goListType = reflect.TypeOf(list.New())
|
||||
var keywordType = reflect.TypeOf(Keyword(""))
|
||||
var symbolType = reflect.TypeOf(Symbol(""))
|
||||
var cmapType = reflect.TypeOf(NewCMap())
|
||||
|
||||
var aUrl, _ = url.Parse("http://foo.com")
|
||||
var urlType = reflect.TypeOf(aUrl)
|
||||
var turiType = reflect.TypeOf(NewTUri("http://example.com"))
|
||||
|
||||
var setType = reflect.TypeOf(Set{})
|
||||
|
||||
var timeType = reflect.TypeOf(time.Now())
|
||||
var bigRatType = reflect.TypeOf(*big.NewRat(int64(1), int64(2)))
|
||||
var bigIntType = reflect.TypeOf(*big.NewInt(int64(1)))
|
||||
var bigFloatType = reflect.TypeOf(*big.NewFloat(float64(1.)))
|
||||
var decimalType = reflect.TypeOf(decimal.NewFromFloat(3))
|
||||
var uuidType = reflect.TypeOf(uuid.NewRandom())
|
||||
var linkType = reflect.TypeOf(*NewLink())
|
||||
var taggedValueType = reflect.TypeOf(TaggedValue{TagId("#foo"), 1})
|
||||
|
||||
var runeType = reflect.TypeOf('x')
|
||||
var nilValue = reflect.ValueOf(nil)
|
||||
var nilEncoder = NewNilEncoder()
|
||||
|
||||
// NewEncoder creates a new encoder set to writ to the stream supplied.
|
||||
// The verbose parameter controls transit's verbose vs non-verbose mode.
|
||||
// Generally for production you want verbose = false.
|
||||
func NewEncoder(w io.Writer, verbose bool) *Encoder {
|
||||
valueEncoders := make(map[interface{}]ValueEncoder)
|
||||
|
||||
var cache Cache
|
||||
|
||||
if verbose {
|
||||
cache = NewNoopCache()
|
||||
} else {
|
||||
cache = NewRollingCache()
|
||||
}
|
||||
|
||||
emitter := NewJsonEmitter(w, cache)
|
||||
e := Encoder{emitter: emitter, valueEncoders: valueEncoders}
|
||||
|
||||
e.addHandler(reflect.String, NewStringEncoder())
|
||||
|
||||
e.addHandler(reflect.Bool, NewBoolEncoder())
|
||||
e.addHandler(reflect.Ptr, NewPointerEncoder())
|
||||
|
||||
floatEncoder := NewFloatEncoder()
|
||||
|
||||
e.addHandler(reflect.Float32, floatEncoder)
|
||||
e.addHandler(reflect.Float64, floatEncoder)
|
||||
|
||||
decimalEncoder := NewDecimalEncoder()
|
||||
e.addHandler(decimalType, decimalEncoder)
|
||||
|
||||
intEncoder := NewIntEncoder()
|
||||
|
||||
e.addHandler(reflect.Int, intEncoder)
|
||||
e.addHandler(reflect.Int8, intEncoder)
|
||||
e.addHandler(reflect.Int16, intEncoder)
|
||||
e.addHandler(reflect.Int32, intEncoder)
|
||||
e.addHandler(reflect.Int64, intEncoder)
|
||||
|
||||
uintEncoder := NewUintEncoder()
|
||||
|
||||
e.addHandler(reflect.Uint, uintEncoder)
|
||||
e.addHandler(reflect.Uint8, uintEncoder)
|
||||
e.addHandler(reflect.Uint16, uintEncoder)
|
||||
e.addHandler(reflect.Uint32, uintEncoder)
|
||||
e.addHandler(reflect.Uint64, uintEncoder)
|
||||
|
||||
arrayEncoder := NewArrayEncoder()
|
||||
|
||||
e.addHandler(reflect.Array, arrayEncoder)
|
||||
e.addHandler(reflect.Slice, arrayEncoder)
|
||||
e.addHandler(reflect.Map, NewMapEncoder(verbose))
|
||||
|
||||
e.addHandler(runeType, NewRuneEncoder())
|
||||
e.addHandler(timeType, NewTimeEncoder())
|
||||
e.addHandler(uuidType, NewUuidEncoder())
|
||||
e.addHandler(bigIntType, NewBigIntEncoder())
|
||||
e.addHandler(bigRatType, NewBigRatEncoder())
|
||||
e.addHandler(bigFloatType, NewBigFloatEncoder())
|
||||
e.addHandler(goListType, NewListEncoder())
|
||||
e.addHandler(symbolType, NewSymbolEncoder())
|
||||
e.addHandler(keywordType, NewKeywordEncoder())
|
||||
e.addHandler(cmapType, NewCMapEncoder())
|
||||
e.addHandler(setType, NewSetEncoder())
|
||||
e.addHandler(urlType, NewUrlEncoder())
|
||||
e.addHandler(turiType, NewTUriEncoder())
|
||||
e.addHandler(linkType, NewLinkEncoder())
|
||||
|
||||
e.addHandler(taggedValueType, NewTaggedValueEncoder())
|
||||
|
||||
return &e
|
||||
}
|
||||
|
||||
// AddHandler adds a new handler to the table used by this encoder
|
||||
// for encoding values. The t value should be an instance
|
||||
// of reflect.Type and the c value should be an encoder for that type.
|
||||
func (e Encoder) AddHandler(t reflect.Type, c ValueEncoder) {
|
||||
e.addHandler(t, c)
|
||||
}
|
||||
|
||||
// addHandler adds a new handler to the table, but the untyped first
|
||||
// parameter lets you enter either reflect.Type or reflect.Kind values.
|
||||
// Used internally.
|
||||
func (e Encoder) addHandler(t interface{}, c ValueEncoder) {
|
||||
e.valueEncoders[t] = c
|
||||
}
|
||||
|
||||
// ValueEncoderFor finds the encoder for the given value.
|
||||
func (e Encoder) ValueEncoderFor(v reflect.Value) ValueEncoder {
|
||||
// Nil is a special case since it doesn't really work
|
||||
// very well with the reflect package.
|
||||
|
||||
if v == nilValue {
|
||||
return nilEncoder
|
||||
}
|
||||
|
||||
// Look for an encoder by the specific type.
|
||||
|
||||
typeEncoder := e.valueEncoders[v.Type()]
|
||||
if typeEncoder != nil {
|
||||
return typeEncoder
|
||||
}
|
||||
|
||||
// If we can't find a type encoder, try finding one
|
||||
// by type. This is will catch values of know kinds,
|
||||
// say int64 or string which have a different specific
|
||||
// type.
|
||||
|
||||
kindEncoder := e.valueEncoders[v.Kind()]
|
||||
if kindEncoder != nil {
|
||||
return kindEncoder
|
||||
}
|
||||
|
||||
// No encoder, for this type, return the error encoder.
|
||||
return NewErrorEncoder()
|
||||
}
|
||||
|
||||
// Given a Value, encode it.
|
||||
func (e Encoder) EncodeValue(v reflect.Value, asKey bool) error {
|
||||
valueEncoder := e.ValueEncoderFor(v)
|
||||
return valueEncoder.Encode(e, v, asKey)
|
||||
}
|
||||
|
||||
// Given a raw interface, encode it.
|
||||
func (e Encoder) EncodeInterface(x interface{}, asKey bool) error {
|
||||
v := reflect.ValueOf(x)
|
||||
return e.EncodeValue(v, asKey)
|
||||
}
|
||||
|
||||
// Encode a value at the top level.
|
||||
func (e Encoder) Encode(x interface{}) error {
|
||||
v := reflect.ValueOf(x)
|
||||
valueEncoder := e.ValueEncoderFor(v)
|
||||
|
||||
if valueEncoder.IsStringable(v) {
|
||||
x = TaggedValue{TagId("'"), x}
|
||||
}
|
||||
|
||||
return e.EncodeInterface(x, false)
|
||||
}
|
||||
|
||||
// Encode the given value to a string.
|
||||
func EncodeToString(x interface{}, verbose bool) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
var encoder = NewEncoder(&buf, verbose)
|
||||
err := encoder.Encode(x)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2016 Russ Olsen. All Rights Reserved.
|
||||
//
|
||||
// This code is a Go port of the Java version created and maintained by Cognitect, therefore:
|
||||
//
|
||||
// Copyright 2014 Cognitect. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package transit
|
||||
|
||||
type TransitError struct {
|
||||
Message string // Describe the error.
|
||||
Source interface{} // The value that cause the problem.
|
||||
}
|
||||
|
||||
func NewTransitError(msg string, v interface{}) *TransitError {
|
||||
return &TransitError{Message: msg, Source: v}
|
||||
}
|
||||
|
||||
func (e *TransitError) Error() string {
|
||||
return e.Message
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright 2016 Russ Olsen. All Rights Reserved.
|
||||
//
|
||||
// This code is a Go port of the Java version created and maintained by Cognitect, therefore:
|
||||
//
|
||||
// Copyright 2014 Cognitect. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package transit
|
||||
|
||||
type Link struct {
|
||||
Href *TUri
|
||||
Rel string
|
||||
Name string
|
||||
Prompt string
|
||||
Render string
|
||||
}
|
||||
|
||||
func NewLink() *Link {
|
||||
return &Link{}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright 2016 Russ Olsen. All Rights Reserved.
|
||||
//
|
||||
// This code is a Go port of the Java version created and maintained by Cognitect, therefore:
|
||||
//
|
||||
// Copyright 2014 Cognitect. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package transit
|
||||
|
||||
// NoopCache is a do-nothing implementation of a cache.
|
||||
type NoopCache struct{}
|
||||
|
||||
func NewNoopCache() *NoopCache {
|
||||
return &NoopCache{}
|
||||
}
|
||||
|
||||
func (c *NoopCache) IsCacheable(s string, asKey bool) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *NoopCache) Write(s string) string {
|
||||
return s
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
// Copyright 2016 Russ Olsen. All Rights Reserved.
|
||||
//
|
||||
// This code is a Go port of the Java version created and maintained by Cognitect, therefore:
|
||||
//
|
||||
// Copyright 2014 Cognitect. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package transit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const cacheCodeDigits = 44
|
||||
const baseCharIndex = 48
|
||||
const firstOrd = 48
|
||||
const cacheSize = (cacheCodeDigits * cacheCodeDigits)
|
||||
const minSizeCacheable = 4
|
||||
|
||||
type StringMap map[string]string
|
||||
|
||||
type RollingCache struct {
|
||||
keyToValue StringMap
|
||||
valueToKey StringMap
|
||||
}
|
||||
|
||||
func NewRollingCache() *RollingCache {
|
||||
return &RollingCache{keyToValue: make(StringMap), valueToKey: make(StringMap)}
|
||||
}
|
||||
|
||||
func (rc *RollingCache) String() string {
|
||||
return fmt.Sprintf("Cache: %v", rc.keyToValue)
|
||||
}
|
||||
|
||||
func (rc *RollingCache) HasKey(name string) bool {
|
||||
_, present := rc.keyToValue[name]
|
||||
return present
|
||||
}
|
||||
|
||||
func (rc *RollingCache) Read(name string) string {
|
||||
return rc.keyToValue[name]
|
||||
}
|
||||
|
||||
// Enter the name into the cache if it passes the cacheable critieria.
|
||||
// Returns either the name or the value that was previously cached for
|
||||
// the name.
|
||||
func (rc *RollingCache) Write(name string) string {
|
||||
existing_key, present := rc.valueToKey[name]
|
||||
|
||||
if present {
|
||||
return existing_key
|
||||
}
|
||||
|
||||
if rc.isCacheFull() {
|
||||
rc.Clear()
|
||||
}
|
||||
|
||||
var key = rc.encodeKey(len(rc.keyToValue))
|
||||
rc.keyToValue[key] = name
|
||||
rc.valueToKey[name] = key
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
// IsCacheable returns true if the string is long enough to be cached
|
||||
// and either asKey is true or the string represents a symbol, keyword
|
||||
// or tag.
|
||||
func (rc *RollingCache) IsCacheable(s string, asKey bool) bool {
|
||||
if len(s) < minSizeCacheable {
|
||||
return false
|
||||
} else if asKey {
|
||||
return true
|
||||
} else {
|
||||
var firstTwo = s[0:2]
|
||||
//return firstTwo == "~#" || firstTwo == "~$" || firstTwo == "~:"
|
||||
return firstTwo == startTag || firstTwo == startKW || firstTwo == startSym
|
||||
}
|
||||
}
|
||||
|
||||
// IsCacheKey returns true if the string looks like a cache key.
|
||||
func (rc *RollingCache) IsCacheKey(name string) bool {
|
||||
if len(name) == 0 {
|
||||
return false
|
||||
} else if (name[0:1] == sub) && (name != mapAsArray) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RollingCache) encodeKey(index int) string {
|
||||
var hi = index / cacheCodeDigits
|
||||
var lo = index % cacheCodeDigits
|
||||
if hi == 0 {
|
||||
return sub + string(lo+baseCharIndex)
|
||||
} else {
|
||||
return sub + string(hi+baseCharIndex) + string(lo+baseCharIndex)
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RollingCache) codeToIndex(s string) int {
|
||||
var sz = len(s)
|
||||
if sz == 2 {
|
||||
return int(s[1]) - baseCharIndex
|
||||
} else {
|
||||
return ((int(s[1]) - baseCharIndex) * cacheCodeDigits) + (int(s[2]) - baseCharIndex)
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RollingCache) isCacheFull() bool {
|
||||
return len(rc.keyToValue) >= cacheSize
|
||||
}
|
||||
|
||||
func (rc *RollingCache) Clear() {
|
||||
rc.valueToKey = make(StringMap)
|
||||
rc.keyToValue = make(StringMap)
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright 2016 Russ Olsen. All Rights Reserved.
|
||||
//
|
||||
// This code is a Go port of the Java version created and maintained by Cognitect, therefore:
|
||||
//
|
||||
// Copyright 2014 Cognitect. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package transit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// A set is a very simple minded representation of a set, one that
|
||||
// does not enforce any redundancy constraints. Just a box
|
||||
// for sets read from transit.
|
||||
type Set struct {
|
||||
Contents []interface{}
|
||||
}
|
||||
|
||||
func MakeSet(contents ...interface{}) *Set {
|
||||
return &Set{Contents: contents}
|
||||
}
|
||||
|
||||
func NewSet(contents []interface{}) *Set {
|
||||
return &Set{Contents: contents}
|
||||
}
|
||||
|
||||
func (s Set) String() string {
|
||||
return fmt.Sprintf("Set[%v]: %v", len(s.Contents), s.Contents)
|
||||
}
|
||||
|
||||
func (s Set) Contains(value interface{}, mf MatchF) bool {
|
||||
for _, element := range s.Contents {
|
||||
if mf(element, value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s Set) ContainsEq(value interface{}) bool {
|
||||
return s.Contains(value, Equals)
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright 2016 Russ Olsen. All Rights Reserved.
|
||||
//
|
||||
// This code is a Go port of the Java version created and maintained by Cognitect, therefore:
|
||||
//
|
||||
// Copyright 2014 Cognitect. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package transit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Cache is the interface for (obviously) caches. Implemented
|
||||
// by RollingCache and NoopCache.
|
||||
type Cache interface {
|
||||
IsCacheable(s string, asKey bool) bool
|
||||
Write(string) string
|
||||
}
|
||||
|
||||
// MatchF is an equality function protocol used by
|
||||
// sets and cmaps.
|
||||
type MatchF func(a, b interface{}) bool
|
||||
|
||||
// Matches keys with a simple == test. Satisfies the
|
||||
// MatchF protocol.
|
||||
func Equals(a, b interface{}) bool {
|
||||
return a == b
|
||||
}
|
||||
|
||||
// A tag id represents a #tag in the transit protocol.
|
||||
type TagId string
|
||||
|
||||
func (t TagId) String() string {
|
||||
return fmt.Sprintf("[Tag: %s]", string(t))
|
||||
}
|
||||
|
||||
// TaggedValue is a simple struct to hold the data from
|
||||
// a transit #tag.
|
||||
|
||||
type TaggedValue struct {
|
||||
Tag TagId
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
// A Keyword is a transit keyword, really just a string by another type.
|
||||
type Keyword string
|
||||
|
||||
func (k Keyword) String() string {
|
||||
return fmt.Sprintf(":%s", string(k))
|
||||
}
|
||||
|
||||
// A Symbol is a transit symbol, really just a string by another type.
|
||||
type Symbol string
|
||||
|
||||
// A TUri is just a container for a uri string. Go url.URL cannot handle all
|
||||
// of the non-ascii chars of transit uris, hence the need for this type.
|
||||
type TUri struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func NewTUri(x string) *TUri {
|
||||
return &TUri{Value: x}
|
||||
}
|
||||
|
||||
func (turi TUri) ToURL() (*url.URL, error) {
|
||||
return url.Parse(turi.Value)
|
||||
}
|
||||
|
||||
func (turi TUri) String() string {
|
||||
return turi.Value
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
// Copyright 2016 Russ Olsen. All Rights Reserved.
|
||||
//
|
||||
// This code is a Go port of the Java version created and maintained by Cognitect, therefore:
|
||||
//
|
||||
// Copyright 2014 Cognitect. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package transit
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// GetMapElement pulls a value out of a map (represented by
|
||||
// the m value). This function differs from the reflect.Value
|
||||
// MapIndex in that the type and kind of the returned value
|
||||
// reflect the type and kind of the element, not what was
|
||||
// declared in the map.
|
||||
func GetMapElement(m reflect.Value, key reflect.Value) reflect.Value {
|
||||
element := m.MapIndex(key)
|
||||
return reflect.ValueOf(element.Interface())
|
||||
}
|
||||
|
||||
// GetElement pulls a value out of a array (represented by
|
||||
// the array value). This function differs from the reflect.Value
|
||||
// Index in that the type and kind of the returned value
|
||||
// reflect the type and kind of the element, not what was
|
||||
// declared in the array.
|
||||
func GetElement(array reflect.Value, i int) reflect.Value {
|
||||
element := array.Index(i)
|
||||
return reflect.ValueOf(element.Interface())
|
||||
}
|
||||
|
||||
// KeyValues returns the Values for the keys in a map.
|
||||
func KeyValues(m reflect.Value) []reflect.Value {
|
||||
keys := m.MapKeys()
|
||||
result := make([]reflect.Value, len(keys))
|
||||
|
||||
for i, v := range keys {
|
||||
result[i] = reflect.ValueOf(v.Interface())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func IsGenericArray(x interface{}) bool {
|
||||
switch x.(type) {
|
||||
case []interface{}:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,276 @@
|
|||
// Copyright 2016 Russ Olsen. All Rights Reserved.
|
||||
//
|
||||
// This code is a Go port of the Java version created and maintained by Cognitect, therefore:
|
||||
//
|
||||
// Copyright 2014 Cognitect. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package transit
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"encoding/base64"
|
||||
"github.com/pborman/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"math"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DecodeKeyword decodes ~: style keywords.
|
||||
func DecodeKeyword(d Decoder, x interface{}) (interface{}, error) {
|
||||
s := x.(string)
|
||||
var result = Keyword(s)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DecodeKeyword decodes ~$ style symbols.
|
||||
func DecodeSymbol(d Decoder, x interface{}) (interface{}, error) {
|
||||
s := x.(string)
|
||||
var result = Symbol(s)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DecodeIdentity returns the value unchanged.
|
||||
func DecodeIdentity(d Decoder, x interface{}) (interface{}, error) {
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// DecodeCMap decodes maps with composite keys.
|
||||
func DecodeCMap(d Decoder, x interface{}) (interface{}, error) {
|
||||
|
||||
tagged := x.(TaggedValue)
|
||||
|
||||
if !IsGenericArray(tagged.Value) {
|
||||
return nil, NewTransitError("Cmap contents are not an array.", tagged)
|
||||
}
|
||||
|
||||
array := tagged.Value.([]interface{})
|
||||
|
||||
if (len(array) % 2) != 0 {
|
||||
return nil, NewTransitError("Cmap contents must contain an even number of elements.", tagged)
|
||||
}
|
||||
|
||||
var result = NewCMap()
|
||||
|
||||
l := len(array)
|
||||
|
||||
for i := 0; i < l; i += 2 {
|
||||
key := array[i]
|
||||
value := array[i+1]
|
||||
result.Append(key, value)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DecodeSet decodes a transit set into a transit.Set instance.
|
||||
func DecodeSet(d Decoder, x interface{}) (interface{}, error) {
|
||||
tagged := x.(TaggedValue)
|
||||
if !IsGenericArray(tagged.Value) {
|
||||
return nil, NewTransitError("Set contents are not an array.", tagged)
|
||||
}
|
||||
values := (tagged.Value).([]interface{})
|
||||
result := NewSet(values)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DecodeList decodes a transit list into a Go list.
|
||||
func DecodeList(d Decoder, x interface{}) (interface{}, error) {
|
||||
tagged := x.(TaggedValue)
|
||||
if !IsGenericArray(tagged.Value) {
|
||||
return nil, NewTransitError("List contents are not an array.", tagged)
|
||||
}
|
||||
values := (tagged.Value).([]interface{})
|
||||
result := list.New()
|
||||
for _, item := range values {
|
||||
result.PushBack(item)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DecodeQuote decodes a transit quoted value by simply returning the value.
|
||||
func DecodeQuote(d Decoder, x interface{}) (interface{}, error) {
|
||||
tagged := x.(TaggedValue)
|
||||
return tagged.Value, nil
|
||||
}
|
||||
|
||||
// DecodeRFC3339 decodes a time value into a Go time instance.
|
||||
// TBD not 100% this covers all possible values.
|
||||
func DecodeRFC3339(d Decoder, x interface{}) (interface{}, error) {
|
||||
s := x.(string)
|
||||
var result, err = time.Parse(time.RFC3339Nano, s)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// DecodeTime decodes a time value represended as millis since 1970.
|
||||
func DecodeTime(d Decoder, x interface{}) (interface{}, error) {
|
||||
s := x.(string)
|
||||
var millis, _ = strconv.ParseInt(s, 10, 64)
|
||||
seconds := millis / 1000
|
||||
remainder_millis := millis - (seconds * 1000)
|
||||
nanos := remainder_millis * 1000000
|
||||
result := time.Unix(seconds, nanos).UTC()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DecodeBoolean decodes a transit boolean into a Go bool.
|
||||
func DecodeBoolean(d Decoder, x interface{}) (interface{}, error) {
|
||||
s := x.(string)
|
||||
if s == "t" {
|
||||
return true, nil
|
||||
} else if s == "f" {
|
||||
return false, nil
|
||||
} else {
|
||||
return nil, &TransitError{Message: "Unknown boolean value."}
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeBigInteger decodes a transit big integer into a Go big.Int.
|
||||
func DecodeBigInteger(d Decoder, x interface{}) (interface{}, error) {
|
||||
s := x.(string)
|
||||
result := new(big.Int)
|
||||
_, good := result.SetString(s, 10)
|
||||
if !good {
|
||||
return nil, &TransitError{Message: "Unable to part big integer: " + s}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DecodeInteger decodes a transit integer into a plain Go int64
|
||||
func DecodeInteger(d Decoder, x interface{}) (interface{}, error) {
|
||||
s := x.(string)
|
||||
result, err := strconv.ParseInt(s, 10, 64)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func newRational(a, b *big.Int) *big.Rat {
|
||||
var r = big.NewRat(1, 1)
|
||||
r.SetFrac(a, b)
|
||||
return r
|
||||
}
|
||||
|
||||
func toBigInt(x interface{}) (*big.Int, error) {
|
||||
switch v := x.(type) {
|
||||
default:
|
||||
return nil, NewTransitError("Not a numeric value", v)
|
||||
case *big.Int:
|
||||
return v, nil
|
||||
case int64:
|
||||
return big.NewInt(v), nil
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeRatio decodes a transit ratio into a Go big.Rat.
|
||||
func DecodeRatio(d Decoder, x interface{}) (interface{}, error) {
|
||||
tagged := x.(TaggedValue)
|
||||
if !IsGenericArray(tagged.Value) {
|
||||
return nil, NewTransitError("Ratio contents are not an array.", tagged)
|
||||
}
|
||||
|
||||
values := (tagged.Value).([]interface{})
|
||||
|
||||
if len(values) != 2 {
|
||||
return nil, NewTransitError("Ratio contents does not contain 2 elements.", tagged)
|
||||
}
|
||||
|
||||
a, err := toBigInt(values[0])
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, err := toBigInt(values[1])
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := newRational(a, b)
|
||||
return *result, nil
|
||||
}
|
||||
|
||||
// DecodeRune decodes a transit char.
|
||||
func DecodeRune(d Decoder, x interface{}) (interface{}, error) {
|
||||
s := x.(string)
|
||||
return rune(s[0]), nil
|
||||
}
|
||||
|
||||
// DecodeFloat decodes the value into a float.
|
||||
func DecodeFloat(d Decoder, x interface{}) (interface{}, error) {
|
||||
s := x.(string)
|
||||
return strconv.ParseFloat(s, 64)
|
||||
}
|
||||
|
||||
// DecodeDecimal decodes a transit big decimal into decimal.Decimal.
|
||||
func DecodeDecimal(d Decoder, x interface{}) (interface{}, error) {
|
||||
s := x.(string)
|
||||
return decimal.NewFromString((s))
|
||||
}
|
||||
|
||||
// DecodeRatio decodes a transit null/nil.
|
||||
func DecodeNil(d Decoder, x interface{}) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// DecodeRatio decodes a transit base64 encoded byte array into a
|
||||
// Go byte array.
|
||||
func DecodeByte(d Decoder, x interface{}) (interface{}, error) {
|
||||
s := x.(string)
|
||||
return base64.StdEncoding.DecodeString(s)
|
||||
}
|
||||
|
||||
// DecodeLink decodes a transit link into an instance of Link.
|
||||
func DecodeLink(d Decoder, x interface{}) (interface{}, error) {
|
||||
tv := x.(TaggedValue)
|
||||
v := tv.Value.(map[interface{}]interface{})
|
||||
l := NewLink()
|
||||
l.Href = v["href"].(*TUri)
|
||||
l.Name = v["name"].(string)
|
||||
l.Rel = v["rel"].(string)
|
||||
l.Prompt = v["prompt"].(string)
|
||||
l.Render = v["render"].(string)
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// DecodeURI decodes a transit URI into an instance of TUri.
|
||||
func DecodeURI(d Decoder, x interface{}) (interface{}, error) {
|
||||
s := x.(string)
|
||||
return NewTUri(s), nil
|
||||
}
|
||||
|
||||
// DecodeUUID decodes a transit UUID into an instance of net/UUID
|
||||
func DecodeUUID(d Decoder, x interface{}) (interface{}, error) {
|
||||
s := x.(string)
|
||||
var u = uuid.Parse(s)
|
||||
if u == nil {
|
||||
return nil, &TransitError{Message: "Unable to parse uuid [" + s + "]"}
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// DecodeSpecialNumber decodes NaN, INF and -INF into their Go equivalents.
|
||||
func DecodeSpecialNumber(d Decoder, x interface{}) (interface{}, error) {
|
||||
tag := x.(string)
|
||||
if tag == "NaN" {
|
||||
return math.NaN(), nil
|
||||
} else if tag == "INF" {
|
||||
return math.Inf(1), nil
|
||||
} else if tag == "-INF" {
|
||||
return math.Inf(-1), nil
|
||||
} else {
|
||||
return nil, &TransitError{Message: "Bad special number:"}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,680 @@
|
|||
// Copyright 2016 Russ Olsen. All Rights Reserved.
|
||||
//
|
||||
// This code is a Go port of the Java version created and maintained by Cognitect, therefore:
|
||||
//
|
||||
// Copyright 2014 Cognitect. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package transit
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"fmt"
|
||||
"github.com/pborman/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"log"
|
||||
"math"
|
||||
"math/big"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ValueEncoder is the interface for objects that know how to
|
||||
// transit encode a single value.
|
||||
type ValueEncoder interface {
|
||||
IsStringable(reflect.Value) bool
|
||||
Encode(e Encoder, value reflect.Value, asString bool) error
|
||||
}
|
||||
|
||||
type NilEncoder struct{}
|
||||
|
||||
func NewNilEncoder() *NilEncoder {
|
||||
return &NilEncoder{}
|
||||
}
|
||||
|
||||
func (ie NilEncoder) IsStringable(v reflect.Value) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ie NilEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
return e.emitter.EmitNil(asKey)
|
||||
}
|
||||
|
||||
type RuneEncoder struct{}
|
||||
|
||||
func NewRuneEncoder() *RuneEncoder {
|
||||
return &RuneEncoder{}
|
||||
}
|
||||
|
||||
func (ie RuneEncoder) IsStringable(v reflect.Value) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ie RuneEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
r := v.Interface().(rune)
|
||||
return e.emitter.EmitString(fmt.Sprintf("~c%c", r), asKey)
|
||||
}
|
||||
|
||||
type PointerEncoder struct{}
|
||||
|
||||
func NewPointerEncoder() *PointerEncoder {
|
||||
return &PointerEncoder{}
|
||||
}
|
||||
|
||||
func (ie PointerEncoder) IsStringable(v reflect.Value) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (ie PointerEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
//log.Println("*** Defer pointer to:", v.Elem())
|
||||
return e.EncodeInterface(v.Elem().Interface(), asKey)
|
||||
}
|
||||
|
||||
type UuidEncoder struct{}
|
||||
|
||||
func NewUuidEncoder() *UuidEncoder {
|
||||
return &UuidEncoder{}
|
||||
}
|
||||
|
||||
func (ie UuidEncoder) IsStringable(v reflect.Value) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ie UuidEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
u := v.Interface().(uuid.UUID)
|
||||
return e.emitter.EmitString(fmt.Sprintf("~u%v", u.String()), asKey)
|
||||
}
|
||||
|
||||
type TimeEncoder struct{}
|
||||
|
||||
func NewTimeEncoder() *TimeEncoder {
|
||||
return &TimeEncoder{}
|
||||
}
|
||||
|
||||
func (ie TimeEncoder) IsStringable(v reflect.Value) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ie TimeEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
t := v.Interface().(time.Time)
|
||||
nanos := t.UnixNano()
|
||||
millis := nanos / int64(1000000)
|
||||
//millis := t.Unix() * 1000
|
||||
return e.emitter.EmitString(fmt.Sprintf("~m%d", millis), asKey)
|
||||
}
|
||||
|
||||
type BoolEncoder struct{}
|
||||
|
||||
func NewBoolEncoder() *BoolEncoder {
|
||||
return &BoolEncoder{}
|
||||
}
|
||||
|
||||
func (ie BoolEncoder) IsStringable(v reflect.Value) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ie BoolEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
b := v.Bool()
|
||||
return e.emitter.EmitBool(b, asKey)
|
||||
}
|
||||
|
||||
type FloatEncoder struct{}
|
||||
|
||||
func NewFloatEncoder() *FloatEncoder {
|
||||
return &FloatEncoder{}
|
||||
}
|
||||
|
||||
func (ie FloatEncoder) IsStringable(v reflect.Value) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ie FloatEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
f := v.Float()
|
||||
if math.IsNaN(f) {
|
||||
return e.emitter.EmitString("~zNaN", asKey)
|
||||
} else if math.IsInf(f, 1) {
|
||||
return e.emitter.EmitString("~zINF", asKey)
|
||||
} else if math.IsInf(f, -1) {
|
||||
return e.emitter.EmitString("~z-INF", asKey)
|
||||
} else {
|
||||
return e.emitter.EmitFloat(f, asKey)
|
||||
}
|
||||
}
|
||||
|
||||
type DecimalEncoder struct{}
|
||||
|
||||
func NewDecimalEncoder() *DecimalEncoder {
|
||||
return &DecimalEncoder{}
|
||||
}
|
||||
|
||||
func (ie DecimalEncoder) IsStringable(v reflect.Value) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ie DecimalEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
f := v.Interface().(decimal.Decimal)
|
||||
return e.emitter.EmitString(fmt.Sprintf("~f%v", f.String()), asKey)
|
||||
}
|
||||
|
||||
type BigFloatEncoder struct{}
|
||||
|
||||
func NewBigFloatEncoder() *BigFloatEncoder {
|
||||
return &BigFloatEncoder{}
|
||||
}
|
||||
|
||||
func (ie BigFloatEncoder) IsStringable(v reflect.Value) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ie BigFloatEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
f := v.Interface().(big.Float)
|
||||
return e.emitter.EmitString(fmt.Sprintf("~f%v", f.Text('f', 25)), asKey)
|
||||
}
|
||||
|
||||
type BigIntEncoder struct{}
|
||||
|
||||
func NewBigIntEncoder() *BigIntEncoder {
|
||||
return &BigIntEncoder{}
|
||||
}
|
||||
|
||||
func (ie BigIntEncoder) IsStringable(v reflect.Value) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ie BigIntEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
i := v.Interface().(big.Int)
|
||||
return e.emitter.EmitString(fmt.Sprintf("~n%v", i.String()), asKey)
|
||||
}
|
||||
|
||||
type BigRatEncoder struct{}
|
||||
|
||||
func NewBigRatEncoder() *BigRatEncoder {
|
||||
return &BigRatEncoder{}
|
||||
}
|
||||
|
||||
func (ie BigRatEncoder) IsStringable(v reflect.Value) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (ie BigRatEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
r := v.Interface().(big.Rat)
|
||||
|
||||
e.emitter.EmitStartArray()
|
||||
e.emitter.EmitTag("ratio")
|
||||
e.emitter.EmitArraySeparator()
|
||||
|
||||
e.emitter.EmitStartArray()
|
||||
e.Encode(r.Num())
|
||||
e.emitter.EmitArraySeparator()
|
||||
e.Encode(r.Denom())
|
||||
e.emitter.EmitEndArray()
|
||||
|
||||
return e.emitter.EmitEndArray()
|
||||
}
|
||||
|
||||
type IntEncoder struct{}
|
||||
|
||||
func NewIntEncoder() *IntEncoder {
|
||||
return &IntEncoder{}
|
||||
}
|
||||
|
||||
func (ie IntEncoder) IsStringable(v reflect.Value) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ie IntEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
return e.emitter.EmitInt(v.Int(), asKey)
|
||||
}
|
||||
|
||||
type UintEncoder struct{}
|
||||
|
||||
func NewUintEncoder() *UintEncoder {
|
||||
return &UintEncoder{}
|
||||
}
|
||||
|
||||
func (ie UintEncoder) IsStringable(v reflect.Value) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ie UintEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
return e.emitter.EmitInt(int64(v.Uint()), asKey)
|
||||
}
|
||||
|
||||
type KeywordEncoder struct{}
|
||||
|
||||
func NewKeywordEncoder() *KeywordEncoder {
|
||||
return &KeywordEncoder{}
|
||||
}
|
||||
|
||||
func (ie KeywordEncoder) IsStringable(v reflect.Value) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ie KeywordEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
s := v.String()
|
||||
//log.Println("Encoding keyword:", s)
|
||||
return e.emitter.EmitString("~:"+s, true)
|
||||
}
|
||||
|
||||
type SymbolEncoder struct{}
|
||||
|
||||
func NewSymbolEncoder() *SymbolEncoder {
|
||||
return &SymbolEncoder{}
|
||||
}
|
||||
|
||||
func (ie SymbolEncoder) IsStringable(v reflect.Value) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ie SymbolEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
s := v.String()
|
||||
//log.Println("Encoding symbol:", s)
|
||||
return e.emitter.EmitString("~$"+s, true)
|
||||
}
|
||||
|
||||
type StringEncoder struct{}
|
||||
|
||||
func NewStringEncoder() *StringEncoder {
|
||||
return &StringEncoder{}
|
||||
}
|
||||
|
||||
func (ie StringEncoder) IsStringable(v reflect.Value) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ie StringEncoder) needsEscape(s string) bool {
|
||||
if len(s) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
firstCh := s[0:1]
|
||||
|
||||
return firstCh == start || firstCh == reserved || firstCh == sub
|
||||
}
|
||||
|
||||
func (ie StringEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
s := v.String()
|
||||
if ie.needsEscape(s) {
|
||||
s = "~" + s
|
||||
}
|
||||
return e.emitter.EmitString(s, asKey)
|
||||
}
|
||||
|
||||
type UrlEncoder struct{}
|
||||
|
||||
func NewUrlEncoder() *UrlEncoder {
|
||||
return &UrlEncoder{}
|
||||
}
|
||||
|
||||
func (ie UrlEncoder) IsStringable(v reflect.Value) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ie UrlEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
u := v.Interface().(*url.URL)
|
||||
us := u.String()
|
||||
return e.emitter.EmitString(fmt.Sprintf("~r%s", us), asKey)
|
||||
}
|
||||
|
||||
type TUriEncoder struct{}
|
||||
|
||||
func NewTUriEncoder() *TUriEncoder {
|
||||
return &TUriEncoder{}
|
||||
}
|
||||
|
||||
func (ie TUriEncoder) IsStringable(v reflect.Value) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ie TUriEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
u := v.Interface().(*TUri)
|
||||
return e.emitter.EmitString(fmt.Sprintf("~r%s", u.Value), asKey)
|
||||
}
|
||||
|
||||
type ErrorEncoder struct{}
|
||||
|
||||
func NewErrorEncoder() *ErrorEncoder {
|
||||
return &ErrorEncoder{}
|
||||
}
|
||||
|
||||
func (ie ErrorEncoder) IsStringable(v reflect.Value) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ie ErrorEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
return NewTransitError("Dont know how to encode value", v)
|
||||
}
|
||||
|
||||
type ArrayEncoder struct{}
|
||||
|
||||
func NewArrayEncoder() *ArrayEncoder {
|
||||
return &ArrayEncoder{}
|
||||
}
|
||||
|
||||
func (ie ArrayEncoder) IsStringable(v reflect.Value) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (ie ArrayEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
e.emitter.EmitStartArray()
|
||||
|
||||
l := v.Len()
|
||||
for i := 0; i < l; i++ {
|
||||
if i > 0 {
|
||||
e.emitter.EmitArraySeparator()
|
||||
}
|
||||
element := v.Index(i)
|
||||
err := e.EncodeInterface(element.Interface(), asKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return e.emitter.EmitEndArray()
|
||||
}
|
||||
|
||||
type MapEncoder struct {
|
||||
verbose bool
|
||||
}
|
||||
|
||||
func NewMapEncoder(verbose bool) *MapEncoder {
|
||||
return &MapEncoder{verbose}
|
||||
}
|
||||
|
||||
func (me MapEncoder) IsStringable(v reflect.Value) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (me MapEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
keys := KeyValues(v)
|
||||
|
||||
if !me.allStringable(e, keys) {
|
||||
return me.encodeCompositeMap(e, v)
|
||||
} else if me.verbose {
|
||||
return me.encodeVerboseMap(e, v)
|
||||
} else {
|
||||
return me.encodeNormalMap(e, v)
|
||||
}
|
||||
}
|
||||
|
||||
func (me MapEncoder) allStringable(e Encoder, keys []reflect.Value) bool {
|
||||
for _, key := range keys {
|
||||
valueEncoder := e.ValueEncoderFor(reflect.ValueOf(key.Interface()))
|
||||
if !valueEncoder.IsStringable(key) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (me MapEncoder) encodeCompositeMap(e Encoder, v reflect.Value) error {
|
||||
e.emitter.EmitStartArray()
|
||||
|
||||
e.emitter.EmitTag("cmap")
|
||||
e.emitter.EmitArraySeparator()
|
||||
|
||||
e.emitter.EmitStartArray()
|
||||
|
||||
keys := KeyValues(v)
|
||||
|
||||
for i, key := range keys {
|
||||
if i != 0 {
|
||||
e.emitter.EmitArraySeparator()
|
||||
}
|
||||
|
||||
err := e.EncodeValue(key, false)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.emitter.EmitArraySeparator()
|
||||
|
||||
value := GetMapElement(v, key)
|
||||
err = e.EncodeValue(value, false)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
e.emitter.EmitEndArray()
|
||||
return e.emitter.EmitEndArray()
|
||||
}
|
||||
|
||||
func (me MapEncoder) encodeNormalMap(e Encoder, v reflect.Value) error {
|
||||
//l := v.Len()
|
||||
e.emitter.EmitStartArray()
|
||||
|
||||
e.emitter.EmitString("^ ", false)
|
||||
|
||||
keys := KeyValues(v)
|
||||
|
||||
for _, key := range keys {
|
||||
e.emitter.EmitArraySeparator()
|
||||
|
||||
err := e.EncodeValue(key, true)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.emitter.EmitArraySeparator()
|
||||
|
||||
value := GetMapElement(v, key)
|
||||
|
||||
err = e.EncodeValue(value, false)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return e.emitter.EmitEndArray()
|
||||
}
|
||||
|
||||
func (me MapEncoder) encodeVerboseMap(e Encoder, v reflect.Value) error {
|
||||
e.emitter.EmitStartMap()
|
||||
|
||||
keys := KeyValues(v)
|
||||
|
||||
for i, key := range keys {
|
||||
if i != 0 {
|
||||
e.emitter.EmitMapSeparator()
|
||||
}
|
||||
|
||||
err := e.EncodeValue(key, true)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.emitter.EmitKeySeparator()
|
||||
|
||||
value := GetMapElement(v, key)
|
||||
|
||||
err = e.EncodeValue(value, false)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return e.emitter.EmitEndMap()
|
||||
}
|
||||
|
||||
type TaggedValueEncoder struct{}
|
||||
|
||||
func NewTaggedValueEncoder() *TaggedValueEncoder {
|
||||
return &TaggedValueEncoder{}
|
||||
}
|
||||
|
||||
func (ie TaggedValueEncoder) IsStringable(v reflect.Value) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (ie TaggedValueEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
t := v.Interface().(TaggedValue)
|
||||
|
||||
e.emitter.EmitStartArray()
|
||||
e.emitter.EmitTag(string(t.Tag))
|
||||
e.emitter.EmitArraySeparator()
|
||||
e.EncodeInterface(t.Value, asKey)
|
||||
return e.emitter.EmitEndArray()
|
||||
}
|
||||
|
||||
type SetEncoder struct{}
|
||||
|
||||
func NewSetEncoder() *SetEncoder {
|
||||
return &SetEncoder{}
|
||||
}
|
||||
|
||||
func (ie SetEncoder) IsStringable(v reflect.Value) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (ie SetEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
s := v.Interface().(Set)
|
||||
|
||||
//log.Println("*** Encode set:", v)
|
||||
|
||||
//l := v.Len()
|
||||
e.emitter.EmitStartArray()
|
||||
e.emitter.EmitTag("set")
|
||||
e.emitter.EmitArraySeparator()
|
||||
|
||||
e.emitter.EmitStartArray()
|
||||
|
||||
for i, element := range s.Contents {
|
||||
if i != 0 {
|
||||
e.emitter.EmitArraySeparator()
|
||||
}
|
||||
err := e.EncodeInterface(element, asKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
e.emitter.EmitEndArray()
|
||||
|
||||
return e.emitter.EmitEndArray()
|
||||
}
|
||||
|
||||
type ListEncoder struct{}
|
||||
|
||||
func NewListEncoder() *ListEncoder {
|
||||
return &ListEncoder{}
|
||||
}
|
||||
|
||||
func (ie ListEncoder) IsStringable(v reflect.Value) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (ie ListEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
lst := v.Interface().(*list.List)
|
||||
|
||||
e.emitter.EmitStartArray()
|
||||
e.emitter.EmitTag("list")
|
||||
e.emitter.EmitArraySeparator()
|
||||
e.emitter.EmitStartArray()
|
||||
|
||||
first := true
|
||||
for element := lst.Front(); element != nil; element = element.Next() {
|
||||
if first {
|
||||
first = false
|
||||
} else {
|
||||
e.emitter.EmitArraySeparator()
|
||||
}
|
||||
|
||||
err := e.EncodeInterface(element.Value, asKey)
|
||||
if err != nil {
|
||||
log.Println("ERROR", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
e.emitter.EmitEndArray()
|
||||
return e.emitter.EmitEndArray()
|
||||
}
|
||||
|
||||
type CMapEncoder struct{}
|
||||
|
||||
func NewCMapEncoder() *CMapEncoder {
|
||||
return &CMapEncoder{}
|
||||
}
|
||||
|
||||
func (ie CMapEncoder) IsStringable(v reflect.Value) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (ie CMapEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
cmap := v.Interface().(*CMap)
|
||||
|
||||
//l := v.Len()
|
||||
e.emitter.EmitStartArray()
|
||||
e.emitter.EmitTag("cmap")
|
||||
e.emitter.EmitArraySeparator()
|
||||
e.emitter.EmitStartArray()
|
||||
|
||||
for i, entry := range cmap.Entries {
|
||||
if i != 0 {
|
||||
e.emitter.EmitArraySeparator()
|
||||
}
|
||||
|
||||
err := e.EncodeInterface(entry.Key, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.emitter.EmitArraySeparator()
|
||||
|
||||
err = e.EncodeInterface(entry.Value, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
e.emitter.EmitEndArray()
|
||||
return e.emitter.EmitEndArray()
|
||||
}
|
||||
|
||||
type LinkEncoder struct{}
|
||||
|
||||
func NewLinkEncoder() *LinkEncoder {
|
||||
return &LinkEncoder{}
|
||||
}
|
||||
|
||||
func (ie LinkEncoder) IsStringable(v reflect.Value) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (ie LinkEncoder) Encode(e Encoder, v reflect.Value, asKey bool) error {
|
||||
link := v.Interface().(*Link)
|
||||
|
||||
e.emitter.EmitStartArray()
|
||||
e.emitter.EmitTag("link")
|
||||
e.emitter.EmitArraySeparator()
|
||||
|
||||
m := map[string]interface{}{
|
||||
"href": link.Href,
|
||||
"rel": link.Rel,
|
||||
"name": link.Name,
|
||||
"prompt": link.Prompt,
|
||||
"render": link.Render,
|
||||
}
|
||||
|
||||
e.Encode(m)
|
||||
return e.emitter.EmitEndArray()
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Spring, Inc.
|
||||
|
||||
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.
|
||||
|
||||
- Based on https://github.com/oguzbilgic/fpd, which has the following license:
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Oguz Bilgic
|
||||
|
||||
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,414 @@
|
|||
// 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.
|
||||
|
||||
// Multiprecision decimal numbers.
|
||||
// For floating-point formatting only; not general purpose.
|
||||
// Only operations are assign and (binary) left/right shift.
|
||||
// Can do binary floating point in multiprecision decimal precisely
|
||||
// because 2 divides 10; cannot do decimal floating point
|
||||
// in multiprecision binary precisely.
|
||||
package decimal
|
||||
|
||||
type decimal struct {
|
||||
d [800]byte // digits, big-endian representation
|
||||
nd int // number of digits used
|
||||
dp int // decimal point
|
||||
neg bool // negative flag
|
||||
trunc bool // discarded nonzero digits beyond d[:nd]
|
||||
}
|
||||
|
||||
func (a *decimal) String() string {
|
||||
n := 10 + a.nd
|
||||
if a.dp > 0 {
|
||||
n += a.dp
|
||||
}
|
||||
if a.dp < 0 {
|
||||
n += -a.dp
|
||||
}
|
||||
|
||||
buf := make([]byte, n)
|
||||
w := 0
|
||||
switch {
|
||||
case a.nd == 0:
|
||||
return "0"
|
||||
|
||||
case a.dp <= 0:
|
||||
// zeros fill space between decimal point and digits
|
||||
buf[w] = '0'
|
||||
w++
|
||||
buf[w] = '.'
|
||||
w++
|
||||
w += digitZero(buf[w : w+-a.dp])
|
||||
w += copy(buf[w:], a.d[0:a.nd])
|
||||
|
||||
case a.dp < a.nd:
|
||||
// decimal point in middle of digits
|
||||
w += copy(buf[w:], a.d[0:a.dp])
|
||||
buf[w] = '.'
|
||||
w++
|
||||
w += copy(buf[w:], a.d[a.dp:a.nd])
|
||||
|
||||
default:
|
||||
// zeros fill space between digits and decimal point
|
||||
w += copy(buf[w:], a.d[0:a.nd])
|
||||
w += digitZero(buf[w : w+a.dp-a.nd])
|
||||
}
|
||||
return string(buf[0:w])
|
||||
}
|
||||
|
||||
func digitZero(dst []byte) int {
|
||||
for i := range dst {
|
||||
dst[i] = '0'
|
||||
}
|
||||
return len(dst)
|
||||
}
|
||||
|
||||
// trim trailing zeros from number.
|
||||
// (They are meaningless; the decimal point is tracked
|
||||
// independent of the number of digits.)
|
||||
func trim(a *decimal) {
|
||||
for a.nd > 0 && a.d[a.nd-1] == '0' {
|
||||
a.nd--
|
||||
}
|
||||
if a.nd == 0 {
|
||||
a.dp = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Assign v to a.
|
||||
func (a *decimal) Assign(v uint64) {
|
||||
var buf [24]byte
|
||||
|
||||
// Write reversed decimal in buf.
|
||||
n := 0
|
||||
for v > 0 {
|
||||
v1 := v / 10
|
||||
v -= 10 * v1
|
||||
buf[n] = byte(v + '0')
|
||||
n++
|
||||
v = v1
|
||||
}
|
||||
|
||||
// Reverse again to produce forward decimal in a.d.
|
||||
a.nd = 0
|
||||
for n--; n >= 0; n-- {
|
||||
a.d[a.nd] = buf[n]
|
||||
a.nd++
|
||||
}
|
||||
a.dp = a.nd
|
||||
trim(a)
|
||||
}
|
||||
|
||||
// Maximum shift that we can do in one pass without overflow.
|
||||
// A uint has 32 or 64 bits, and we have to be able to accommodate 9<<k.
|
||||
const uintSize = 32 << (^uint(0) >> 63)
|
||||
const maxShift = uintSize - 4
|
||||
|
||||
// Binary shift right (/ 2) by k bits. k <= maxShift to avoid overflow.
|
||||
func rightShift(a *decimal, k uint) {
|
||||
r := 0 // read pointer
|
||||
w := 0 // write pointer
|
||||
|
||||
// Pick up enough leading digits to cover first shift.
|
||||
var n uint
|
||||
for ; n>>k == 0; r++ {
|
||||
if r >= a.nd {
|
||||
if n == 0 {
|
||||
// a == 0; shouldn't get here, but handle anyway.
|
||||
a.nd = 0
|
||||
return
|
||||
}
|
||||
for n>>k == 0 {
|
||||
n = n * 10
|
||||
r++
|
||||
}
|
||||
break
|
||||
}
|
||||
c := uint(a.d[r])
|
||||
n = n*10 + c - '0'
|
||||
}
|
||||
a.dp -= r - 1
|
||||
|
||||
var mask uint = (1 << k) - 1
|
||||
|
||||
// Pick up a digit, put down a digit.
|
||||
for ; r < a.nd; r++ {
|
||||
c := uint(a.d[r])
|
||||
dig := n >> k
|
||||
n &= mask
|
||||
a.d[w] = byte(dig + '0')
|
||||
w++
|
||||
n = n*10 + c - '0'
|
||||
}
|
||||
|
||||
// Put down extra digits.
|
||||
for n > 0 {
|
||||
dig := n >> k
|
||||
n &= mask
|
||||
if w < len(a.d) {
|
||||
a.d[w] = byte(dig + '0')
|
||||
w++
|
||||
} else if dig > 0 {
|
||||
a.trunc = true
|
||||
}
|
||||
n = n * 10
|
||||
}
|
||||
|
||||
a.nd = w
|
||||
trim(a)
|
||||
}
|
||||
|
||||
// Cheat sheet for left shift: table indexed by shift count giving
|
||||
// number of new digits that will be introduced by that shift.
|
||||
//
|
||||
// For example, leftcheats[4] = {2, "625"}. That means that
|
||||
// if we are shifting by 4 (multiplying by 16), it will add 2 digits
|
||||
// when the string prefix is "625" through "999", and one fewer digit
|
||||
// if the string prefix is "000" through "624".
|
||||
//
|
||||
// Credit for this trick goes to Ken.
|
||||
|
||||
type leftCheat struct {
|
||||
delta int // number of new digits
|
||||
cutoff string // minus one digit if original < a.
|
||||
}
|
||||
|
||||
var leftcheats = []leftCheat{
|
||||
// Leading digits of 1/2^i = 5^i.
|
||||
// 5^23 is not an exact 64-bit floating point number,
|
||||
// so have to use bc for the math.
|
||||
// Go up to 60 to be large enough for 32bit and 64bit platforms.
|
||||
/*
|
||||
seq 60 | sed 's/^/5^/' | bc |
|
||||
awk 'BEGIN{ print "\t{ 0, \"\" }," }
|
||||
{
|
||||
log2 = log(2)/log(10)
|
||||
printf("\t{ %d, \"%s\" },\t// * %d\n",
|
||||
int(log2*NR+1), $0, 2**NR)
|
||||
}'
|
||||
*/
|
||||
{0, ""},
|
||||
{1, "5"}, // * 2
|
||||
{1, "25"}, // * 4
|
||||
{1, "125"}, // * 8
|
||||
{2, "625"}, // * 16
|
||||
{2, "3125"}, // * 32
|
||||
{2, "15625"}, // * 64
|
||||
{3, "78125"}, // * 128
|
||||
{3, "390625"}, // * 256
|
||||
{3, "1953125"}, // * 512
|
||||
{4, "9765625"}, // * 1024
|
||||
{4, "48828125"}, // * 2048
|
||||
{4, "244140625"}, // * 4096
|
||||
{4, "1220703125"}, // * 8192
|
||||
{5, "6103515625"}, // * 16384
|
||||
{5, "30517578125"}, // * 32768
|
||||
{5, "152587890625"}, // * 65536
|
||||
{6, "762939453125"}, // * 131072
|
||||
{6, "3814697265625"}, // * 262144
|
||||
{6, "19073486328125"}, // * 524288
|
||||
{7, "95367431640625"}, // * 1048576
|
||||
{7, "476837158203125"}, // * 2097152
|
||||
{7, "2384185791015625"}, // * 4194304
|
||||
{7, "11920928955078125"}, // * 8388608
|
||||
{8, "59604644775390625"}, // * 16777216
|
||||
{8, "298023223876953125"}, // * 33554432
|
||||
{8, "1490116119384765625"}, // * 67108864
|
||||
{9, "7450580596923828125"}, // * 134217728
|
||||
{9, "37252902984619140625"}, // * 268435456
|
||||
{9, "186264514923095703125"}, // * 536870912
|
||||
{10, "931322574615478515625"}, // * 1073741824
|
||||
{10, "4656612873077392578125"}, // * 2147483648
|
||||
{10, "23283064365386962890625"}, // * 4294967296
|
||||
{10, "116415321826934814453125"}, // * 8589934592
|
||||
{11, "582076609134674072265625"}, // * 17179869184
|
||||
{11, "2910383045673370361328125"}, // * 34359738368
|
||||
{11, "14551915228366851806640625"}, // * 68719476736
|
||||
{12, "72759576141834259033203125"}, // * 137438953472
|
||||
{12, "363797880709171295166015625"}, // * 274877906944
|
||||
{12, "1818989403545856475830078125"}, // * 549755813888
|
||||
{13, "9094947017729282379150390625"}, // * 1099511627776
|
||||
{13, "45474735088646411895751953125"}, // * 2199023255552
|
||||
{13, "227373675443232059478759765625"}, // * 4398046511104
|
||||
{13, "1136868377216160297393798828125"}, // * 8796093022208
|
||||
{14, "5684341886080801486968994140625"}, // * 17592186044416
|
||||
{14, "28421709430404007434844970703125"}, // * 35184372088832
|
||||
{14, "142108547152020037174224853515625"}, // * 70368744177664
|
||||
{15, "710542735760100185871124267578125"}, // * 140737488355328
|
||||
{15, "3552713678800500929355621337890625"}, // * 281474976710656
|
||||
{15, "17763568394002504646778106689453125"}, // * 562949953421312
|
||||
{16, "88817841970012523233890533447265625"}, // * 1125899906842624
|
||||
{16, "444089209850062616169452667236328125"}, // * 2251799813685248
|
||||
{16, "2220446049250313080847263336181640625"}, // * 4503599627370496
|
||||
{16, "11102230246251565404236316680908203125"}, // * 9007199254740992
|
||||
{17, "55511151231257827021181583404541015625"}, // * 18014398509481984
|
||||
{17, "277555756156289135105907917022705078125"}, // * 36028797018963968
|
||||
{17, "1387778780781445675529539585113525390625"}, // * 72057594037927936
|
||||
{18, "6938893903907228377647697925567626953125"}, // * 144115188075855872
|
||||
{18, "34694469519536141888238489627838134765625"}, // * 288230376151711744
|
||||
{18, "173472347597680709441192448139190673828125"}, // * 576460752303423488
|
||||
{19, "867361737988403547205962240695953369140625"}, // * 1152921504606846976
|
||||
}
|
||||
|
||||
// Is the leading prefix of b lexicographically less than s?
|
||||
func prefixIsLessThan(b []byte, s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if i >= len(b) {
|
||||
return true
|
||||
}
|
||||
if b[i] != s[i] {
|
||||
return b[i] < s[i]
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Binary shift left (* 2) by k bits. k <= maxShift to avoid overflow.
|
||||
func leftShift(a *decimal, k uint) {
|
||||
delta := leftcheats[k].delta
|
||||
if prefixIsLessThan(a.d[0:a.nd], leftcheats[k].cutoff) {
|
||||
delta--
|
||||
}
|
||||
|
||||
r := a.nd // read index
|
||||
w := a.nd + delta // write index
|
||||
|
||||
// Pick up a digit, put down a digit.
|
||||
var n uint
|
||||
for r--; r >= 0; r-- {
|
||||
n += (uint(a.d[r]) - '0') << k
|
||||
quo := n / 10
|
||||
rem := n - 10*quo
|
||||
w--
|
||||
if w < len(a.d) {
|
||||
a.d[w] = byte(rem + '0')
|
||||
} else if rem != 0 {
|
||||
a.trunc = true
|
||||
}
|
||||
n = quo
|
||||
}
|
||||
|
||||
// Put down extra digits.
|
||||
for n > 0 {
|
||||
quo := n / 10
|
||||
rem := n - 10*quo
|
||||
w--
|
||||
if w < len(a.d) {
|
||||
a.d[w] = byte(rem + '0')
|
||||
} else if rem != 0 {
|
||||
a.trunc = true
|
||||
}
|
||||
n = quo
|
||||
}
|
||||
|
||||
a.nd += delta
|
||||
if a.nd >= len(a.d) {
|
||||
a.nd = len(a.d)
|
||||
}
|
||||
a.dp += delta
|
||||
trim(a)
|
||||
}
|
||||
|
||||
// Binary shift left (k > 0) or right (k < 0).
|
||||
func (a *decimal) Shift(k int) {
|
||||
switch {
|
||||
case a.nd == 0:
|
||||
// nothing to do: a == 0
|
||||
case k > 0:
|
||||
for k > maxShift {
|
||||
leftShift(a, maxShift)
|
||||
k -= maxShift
|
||||
}
|
||||
leftShift(a, uint(k))
|
||||
case k < 0:
|
||||
for k < -maxShift {
|
||||
rightShift(a, maxShift)
|
||||
k += maxShift
|
||||
}
|
||||
rightShift(a, uint(-k))
|
||||
}
|
||||
}
|
||||
|
||||
// If we chop a at nd digits, should we round up?
|
||||
func shouldRoundUp(a *decimal, nd int) bool {
|
||||
if nd < 0 || nd >= a.nd {
|
||||
return false
|
||||
}
|
||||
if a.d[nd] == '5' && nd+1 == a.nd { // exactly halfway - round to even
|
||||
// if we truncated, a little higher than what's recorded - always round up
|
||||
if a.trunc {
|
||||
return true
|
||||
}
|
||||
return nd > 0 && (a.d[nd-1]-'0')%2 != 0
|
||||
}
|
||||
// not halfway - digit tells all
|
||||
return a.d[nd] >= '5'
|
||||
}
|
||||
|
||||
// Round a to nd digits (or fewer).
|
||||
// If nd is zero, it means we're rounding
|
||||
// just to the left of the digits, as in
|
||||
// 0.09 -> 0.1.
|
||||
func (a *decimal) Round(nd int) {
|
||||
if nd < 0 || nd >= a.nd {
|
||||
return
|
||||
}
|
||||
if shouldRoundUp(a, nd) {
|
||||
a.RoundUp(nd)
|
||||
} else {
|
||||
a.RoundDown(nd)
|
||||
}
|
||||
}
|
||||
|
||||
// Round a down to nd digits (or fewer).
|
||||
func (a *decimal) RoundDown(nd int) {
|
||||
if nd < 0 || nd >= a.nd {
|
||||
return
|
||||
}
|
||||
a.nd = nd
|
||||
trim(a)
|
||||
}
|
||||
|
||||
// Round a up to nd digits (or fewer).
|
||||
func (a *decimal) RoundUp(nd int) {
|
||||
if nd < 0 || nd >= a.nd {
|
||||
return
|
||||
}
|
||||
|
||||
// round up
|
||||
for i := nd - 1; i >= 0; i-- {
|
||||
c := a.d[i]
|
||||
if c < '9' { // can stop after this digit
|
||||
a.d[i]++
|
||||
a.nd = i + 1
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Number is all 9s.
|
||||
// Change to single 1 with adjusted decimal point.
|
||||
a.d[0] = '1'
|
||||
a.nd = 1
|
||||
a.dp++
|
||||
}
|
||||
|
||||
// Extract integer part, rounded appropriately.
|
||||
// No guarantees about overflow.
|
||||
func (a *decimal) RoundedInteger() uint64 {
|
||||
if a.dp > 20 {
|
||||
return 0xFFFFFFFFFFFFFFFF
|
||||
}
|
||||
var i int
|
||||
n := uint64(0)
|
||||
for i = 0; i < a.dp && i < a.nd; i++ {
|
||||
n = n*10 + uint64(a.d[i]-'0')
|
||||
}
|
||||
for ; i < a.dp; i++ {
|
||||
n *= 10
|
||||
}
|
||||
if shouldRoundUp(a, a.dp) {
|
||||
n++
|
||||
}
|
||||
return n
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,118 @@
|
|||
// 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.
|
||||
|
||||
// Multiprecision decimal numbers.
|
||||
// For floating-point formatting only; not general purpose.
|
||||
// Only operations are assign and (binary) left/right shift.
|
||||
// Can do binary floating point in multiprecision decimal precisely
|
||||
// because 2 divides 10; cannot do decimal floating point
|
||||
// in multiprecision binary precisely.
|
||||
package decimal
|
||||
|
||||
type floatInfo struct {
|
||||
mantbits uint
|
||||
expbits uint
|
||||
bias int
|
||||
}
|
||||
|
||||
var float32info = floatInfo{23, 8, -127}
|
||||
var float64info = floatInfo{52, 11, -1023}
|
||||
|
||||
// roundShortest rounds d (= mant * 2^exp) to the shortest number of digits
|
||||
// that will let the original floating point value be precisely reconstructed.
|
||||
func roundShortest(d *decimal, mant uint64, exp int, flt *floatInfo) {
|
||||
// If mantissa is zero, the number is zero; stop now.
|
||||
if mant == 0 {
|
||||
d.nd = 0
|
||||
return
|
||||
}
|
||||
|
||||
// Compute upper and lower such that any decimal number
|
||||
// between upper and lower (possibly inclusive)
|
||||
// will round to the original floating point number.
|
||||
|
||||
// We may see at once that the number is already shortest.
|
||||
//
|
||||
// Suppose d is not denormal, so that 2^exp <= d < 10^dp.
|
||||
// The closest shorter number is at least 10^(dp-nd) away.
|
||||
// The lower/upper bounds computed below are at distance
|
||||
// at most 2^(exp-mantbits).
|
||||
//
|
||||
// So the number is already shortest if 10^(dp-nd) > 2^(exp-mantbits),
|
||||
// or equivalently log2(10)*(dp-nd) > exp-mantbits.
|
||||
// It is true if 332/100*(dp-nd) >= exp-mantbits (log2(10) > 3.32).
|
||||
minexp := flt.bias + 1 // minimum possible exponent
|
||||
if exp > minexp && 332*(d.dp-d.nd) >= 100*(exp-int(flt.mantbits)) {
|
||||
// The number is already shortest.
|
||||
return
|
||||
}
|
||||
|
||||
// d = mant << (exp - mantbits)
|
||||
// Next highest floating point number is mant+1 << exp-mantbits.
|
||||
// Our upper bound is halfway between, mant*2+1 << exp-mantbits-1.
|
||||
upper := new(decimal)
|
||||
upper.Assign(mant*2 + 1)
|
||||
upper.Shift(exp - int(flt.mantbits) - 1)
|
||||
|
||||
// d = mant << (exp - mantbits)
|
||||
// Next lowest floating point number is mant-1 << exp-mantbits,
|
||||
// unless mant-1 drops the significant bit and exp is not the minimum exp,
|
||||
// in which case the next lowest is mant*2-1 << exp-mantbits-1.
|
||||
// Either way, call it mantlo << explo-mantbits.
|
||||
// Our lower bound is halfway between, mantlo*2+1 << explo-mantbits-1.
|
||||
var mantlo uint64
|
||||
var explo int
|
||||
if mant > 1<<flt.mantbits || exp == minexp {
|
||||
mantlo = mant - 1
|
||||
explo = exp
|
||||
} else {
|
||||
mantlo = mant*2 - 1
|
||||
explo = exp - 1
|
||||
}
|
||||
lower := new(decimal)
|
||||
lower.Assign(mantlo*2 + 1)
|
||||
lower.Shift(explo - int(flt.mantbits) - 1)
|
||||
|
||||
// The upper and lower bounds are possible outputs only if
|
||||
// the original mantissa is even, so that IEEE round-to-even
|
||||
// would round to the original mantissa and not the neighbors.
|
||||
inclusive := mant%2 == 0
|
||||
|
||||
// Now we can figure out the minimum number of digits required.
|
||||
// Walk along until d has distinguished itself from upper and lower.
|
||||
for i := 0; i < d.nd; i++ {
|
||||
l := byte('0') // lower digit
|
||||
if i < lower.nd {
|
||||
l = lower.d[i]
|
||||
}
|
||||
m := d.d[i] // middle digit
|
||||
u := byte('0') // upper digit
|
||||
if i < upper.nd {
|
||||
u = upper.d[i]
|
||||
}
|
||||
|
||||
// Okay to round down (truncate) if lower has a different digit
|
||||
// or if lower is inclusive and is exactly the result of rounding
|
||||
// down (i.e., and we have reached the final digit of lower).
|
||||
okdown := l != m || inclusive && i+1 == lower.nd
|
||||
|
||||
// Okay to round up if upper has a different digit and either upper
|
||||
// is inclusive or upper is bigger than the result of rounding up.
|
||||
okup := m != u && (inclusive || m+1 < u || i+1 < upper.nd)
|
||||
|
||||
// If it's okay to do either, then round to the nearest one.
|
||||
// If it's okay to do only one, do it.
|
||||
switch {
|
||||
case okdown && okup:
|
||||
d.Round(i + 1)
|
||||
return
|
||||
case okdown:
|
||||
d.RoundDown(i + 1)
|
||||
return
|
||||
case okup:
|
||||
d.RoundUp(i + 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue