563 lines
19 KiB
Go
563 lines
19 KiB
Go
package companion
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
var errAdapterUnsupported = errors.New("runtime capability is unavailable")
|
|
|
|
// AuthorizedConfigPort is supplied through the platform-authorized Run channel.
|
|
// It exposes logical, allowlisted configuration values only; it never exposes a
|
|
// path, DSN, credential, arbitrary command, or database handle.
|
|
type AuthorizedConfigPort interface {
|
|
ReadConfig(context.Context) (map[string]string, error)
|
|
ApplyConfigPatch(context.Context, string, []ConfigFieldPatch) (map[string]string, error)
|
|
}
|
|
type ConfigFieldPatch struct {
|
|
Key string
|
|
Value string
|
|
}
|
|
|
|
// AuthorizedGameDataPort is a typed, Run-owned read/patch boundary. Implementations
|
|
// must probe their local schema, use field allowlists and safe windows, and return
|
|
// bounded snapshots rather than rows or connection details.
|
|
type AuthorizedGameDataPort interface {
|
|
ReadPlayerState(context.Context, string, []string) (PlayerStateSnapshot, error)
|
|
ApplyPlayerState(context.Context, PlayerStatePatch) (PlayerStateSnapshot, error)
|
|
}
|
|
type PlayerStateSnapshot struct {
|
|
PlayerID string
|
|
StateVersion string
|
|
SafeWindow bool
|
|
Fields map[string]float64
|
|
}
|
|
type PlayerStatePatch struct {
|
|
PlayerID string
|
|
ExpectedStateVersion string
|
|
Fields []StateFieldPatch
|
|
}
|
|
type StateFieldPatch struct {
|
|
Key string
|
|
Before float64
|
|
After float64
|
|
}
|
|
|
|
// AuthorizedRewardPort accepts a frozen grant with typed items and operations.
|
|
type AuthorizedRewardPort interface {
|
|
DeliverReward(context.Context, RewardGrant) (DeliveryReceipt, error)
|
|
}
|
|
type RewardGrant struct {
|
|
GrantID string
|
|
PlayerID string
|
|
Items []RewardItem
|
|
Operations []string
|
|
}
|
|
type RewardItem struct {
|
|
CatalogCode string
|
|
Quantity int
|
|
}
|
|
type DeliveryReceipt struct {
|
|
Outcome string
|
|
DeliveryID string
|
|
}
|
|
|
|
type AuthorizedEventPort interface {
|
|
StartEvent(context.Context, EventStartRequest) (EventStartReceipt, error)
|
|
}
|
|
type EventStartRequest struct {
|
|
EventID string
|
|
EventType string
|
|
Class int
|
|
Title string
|
|
Placard string
|
|
Percent int
|
|
NPC int
|
|
Item int
|
|
Zombie int
|
|
Animal int
|
|
Produces []EventProduceRequest
|
|
DurationSeconds int
|
|
MaxParticipants int
|
|
Announce bool
|
|
}
|
|
type EventProduceRequest struct {
|
|
TradeGoodsID string
|
|
Percent int
|
|
Value int
|
|
Radius float64
|
|
X float64
|
|
Y float64
|
|
Z float64
|
|
}
|
|
type EventStartReceipt struct {
|
|
Accepted bool
|
|
Status string
|
|
EventID string
|
|
Message string
|
|
}
|
|
|
|
const fixedNotificationType = 4
|
|
|
|
type UE4SSNotificationPort interface {
|
|
SendPlayerNotification(context.Context, ue4SSPlayerNotification) (UE4SSNotificationReceipt, error)
|
|
}
|
|
type UE4SSNotificationReceipt struct{ Accepted bool }
|
|
type ue4SSPlayerNotification struct {
|
|
ServerID string
|
|
RecipientSteamID string
|
|
Message string
|
|
chatType int
|
|
protectedAuditCommand string
|
|
}
|
|
type UE4SSVehicleSpawnPort interface {
|
|
SpawnVehicle(context.Context, ue4SSVehicleSpawn) (UE4SSVehicleSpawnReceipt, error)
|
|
}
|
|
type UE4SSVehicleSpawnOutcome string
|
|
|
|
const (
|
|
UE4SSVehicleSpawnAccepted UE4SSVehicleSpawnOutcome = "accepted"
|
|
UE4SSVehicleSpawnRejected UE4SSVehicleSpawnOutcome = "rejected"
|
|
UE4SSVehicleSpawnUnknown UE4SSVehicleSpawnOutcome = "unknown"
|
|
)
|
|
|
|
type UE4SSVehicleSpawnReceipt struct{ Outcome UE4SSVehicleSpawnOutcome }
|
|
type ue4SSVehicleSpawn struct {
|
|
ServerID string
|
|
VehicleCode string
|
|
protectedAuditCommand string
|
|
}
|
|
|
|
// RuntimeAdapter is bound to one server. Availability is discovered from its
|
|
// configured typed ports and declared capabilities. A failed probe affects
|
|
// only its operation.
|
|
type RuntimeAdapter struct {
|
|
BoundServerID string
|
|
Config AuthorizedConfigPort
|
|
GameData AuthorizedGameDataPort
|
|
Rewards AuthorizedRewardPort
|
|
Events AuthorizedEventPort
|
|
Notification UE4SSNotificationPort
|
|
VehicleSpawn UE4SSVehicleSpawnPort
|
|
DiagnosticsState map[string]string
|
|
}
|
|
|
|
func (adapter RuntimeAdapter) ServerBinding() string { return adapter.BoundServerID }
|
|
|
|
func (adapter RuntimeAdapter) ReadConfiguration(ctx context.Context) (map[string]any, error) {
|
|
if adapter.Config == nil {
|
|
return nil, errAdapterUnsupported
|
|
}
|
|
fields, err := adapter.Config.ReadConfig(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"fields": redactConfigValues(fields)}, nil
|
|
}
|
|
func (adapter RuntimeAdapter) PatchConfiguration(ctx context.Context, payload map[string]any) (map[string]any, error) {
|
|
if adapter.Config == nil {
|
|
return nil, errAdapterUnsupported
|
|
}
|
|
revision, _ := payload["revision"].(string)
|
|
raw, _ := payload["fields"].([]any)
|
|
fields := make([]ConfigFieldPatch, 0, len(raw))
|
|
for _, value := range raw {
|
|
item, ok := value.(map[string]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("configuration patch payload is invalid")
|
|
}
|
|
key, keyOK := item["key"].(string)
|
|
fieldValue, valueOK := item["value"].(string)
|
|
if !keyOK || !valueOK || !supportedConfigKey(key) {
|
|
return nil, fmt.Errorf("configuration patch field is unsupported")
|
|
}
|
|
fields = append(fields, ConfigFieldPatch{Key: key, Value: fieldValue})
|
|
}
|
|
applied, err := adapter.Config.ApplyConfigPatch(ctx, revision, fields)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"appliedFields": redactConfigValues(applied)}, nil
|
|
}
|
|
func (adapter RuntimeAdapter) Diagnostics(context.Context) (map[string]any, error) {
|
|
state := map[string]any{"adapter": "runtime-capability"}
|
|
for key, value := range adapter.DiagnosticsState {
|
|
if safeDiagnosticField(key, value) {
|
|
state[key] = value
|
|
}
|
|
}
|
|
return state, nil
|
|
}
|
|
func (adapter RuntimeAdapter) PatchGameState(ctx context.Context, payload map[string]any) (map[string]any, error) {
|
|
if adapter.GameData == nil {
|
|
return nil, errAdapterUnsupported
|
|
}
|
|
playerID, _ := payload["playerId"].(string)
|
|
expected, _ := payload["expectedStateVersion"].(string)
|
|
raw, _ := payload["changes"].([]any)
|
|
fields, err := statePatchFields(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
before, err := adapter.GameData.ReadPlayerState(ctx, playerID, statePatchKeys(fields))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if before.PlayerID != playerID || before.StateVersion != expected || !before.SafeWindow || !stateMatches(before.Fields, fields) {
|
|
return map[string]any{"outcome": "failed"}, nil
|
|
}
|
|
after, err := adapter.GameData.ApplyPlayerState(ctx, PlayerStatePatch{PlayerID: playerID, ExpectedStateVersion: expected, Fields: fields})
|
|
if err != nil {
|
|
return map[string]any{"outcome": "unknown"}, nil
|
|
}
|
|
confirmed, err := adapter.GameData.ReadPlayerState(ctx, playerID, statePatchKeys(fields))
|
|
if err != nil || after.PlayerID != playerID || !stateApplied(confirmed.Fields, fields) {
|
|
return map[string]any{"outcome": "unknown"}, nil
|
|
}
|
|
return map[string]any{"outcome": "succeeded", "changedFields": len(fields)}, nil
|
|
}
|
|
func (adapter RuntimeAdapter) DeliverReward(ctx context.Context, payload map[string]any) (map[string]any, error) {
|
|
if adapter.Rewards == nil {
|
|
return nil, errAdapterUnsupported
|
|
}
|
|
grant, err := rewardGrant(payload)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
receipt, err := adapter.Rewards.DeliverReward(ctx, grant)
|
|
if err != nil || receipt.Outcome == "unknown" {
|
|
return map[string]any{"accepted": false, "status": "rejected", "message": "reward delivery result is unknown"}, nil
|
|
}
|
|
if receipt.Outcome != "delivered" {
|
|
return map[string]any{"accepted": false, "status": "rejected"}, nil
|
|
}
|
|
result := map[string]any{"accepted": true, "status": "delivered"}
|
|
if receipt.DeliveryID != "" {
|
|
result["deliveryId"] = receipt.DeliveryID
|
|
}
|
|
return result, nil
|
|
}
|
|
func (adapter RuntimeAdapter) StartEvent(ctx context.Context, payload map[string]any) (map[string]any, error) {
|
|
if adapter.Events == nil {
|
|
return nil, errAdapterUnsupported
|
|
}
|
|
request, err := eventStartRequest(payload)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
receipt, err := adapter.Events.StartEvent(ctx, request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
status := receipt.Status
|
|
if status == "" {
|
|
status = "rejected"
|
|
}
|
|
if status != "started" && status != "queued" && status != "rejected" {
|
|
return nil, fmt.Errorf("event start receipt is invalid")
|
|
}
|
|
eventID := receipt.EventID
|
|
if eventID == "" {
|
|
eventID = request.EventID
|
|
}
|
|
result := map[string]any{"accepted": receipt.Accepted, "status": status, "eventId": eventID}
|
|
if receipt.Message != "" {
|
|
result["message"] = receipt.Message
|
|
}
|
|
return result, nil
|
|
}
|
|
func (adapter RuntimeAdapter) NotifyPlayer(ctx context.Context, payload map[string]any) (map[string]any, error) {
|
|
if adapter.BoundServerID == "" || adapter.Notification == nil {
|
|
return nil, errAdapterUnsupported
|
|
}
|
|
playerID, playerOK := payload["playerId"].(string)
|
|
message, messageOK := payload["message"].(string)
|
|
notification, err := newUE4SSPlayerNotification(adapter.BoundServerID, playerID, message)
|
|
if !playerOK || !messageOK || err != nil {
|
|
return nil, fmt.Errorf("notification payload is invalid")
|
|
}
|
|
receipt, err := adapter.Notification.SendPlayerNotification(ctx, notification)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("notification transport failed")
|
|
}
|
|
return map[string]any{"accepted": receipt.Accepted}, nil
|
|
}
|
|
func (adapter RuntimeAdapter) SpawnVehicle(ctx context.Context, payload map[string]any) (map[string]any, error) {
|
|
if adapter.BoundServerID == "" || adapter.VehicleSpawn == nil {
|
|
return nil, errAdapterUnsupported
|
|
}
|
|
vehicleCode, ok := payload["vehicleCode"].(string)
|
|
spawn, err := newUE4SSVehicleSpawn(adapter.BoundServerID, vehicleCode)
|
|
if !ok || err != nil {
|
|
return nil, fmt.Errorf("vehicle spawn payload is invalid")
|
|
}
|
|
receipt, err := adapter.VehicleSpawn.SpawnVehicle(ctx, spawn)
|
|
if err != nil || receipt.Outcome == UE4SSVehicleSpawnUnknown {
|
|
return map[string]any{"outcome": "unknown"}, nil
|
|
}
|
|
if receipt.Outcome == UE4SSVehicleSpawnRejected {
|
|
return map[string]any{"outcome": "failed"}, nil
|
|
}
|
|
if receipt.Outcome != UE4SSVehicleSpawnAccepted {
|
|
return map[string]any{"outcome": "unknown"}, nil
|
|
}
|
|
return map[string]any{"outcome": "succeeded"}, nil
|
|
}
|
|
func newUE4SSPlayerNotification(serverID, playerID, message string) (ue4SSPlayerNotification, error) {
|
|
if strings.TrimSpace(serverID) == "" || !steamID64(playerID) || !validNotificationMessage(message) {
|
|
return ue4SSPlayerNotification{}, fmt.Errorf("invalid typed notification")
|
|
}
|
|
return ue4SSPlayerNotification{ServerID: serverID, RecipientSteamID: playerID, Message: message, chatType: fixedNotificationType, protectedAuditCommand: "SendChat 4 \"" + escapeUE4SSChatMessage(message) + "\" " + playerID}, nil
|
|
}
|
|
func newUE4SSVehicleSpawn(serverID, vehicleCode string) (ue4SSVehicleSpawn, error) {
|
|
if strings.TrimSpace(serverID) == "" || !supportedVehicleSpawnCode(vehicleCode) {
|
|
return ue4SSVehicleSpawn{}, fmt.Errorf("invalid typed vehicle spawn")
|
|
}
|
|
return ue4SSVehicleSpawn{ServerID: serverID, VehicleCode: vehicleCode, protectedAuditCommand: "#spawnvehicle " + vehicleCode}, nil
|
|
}
|
|
func steamID64(value string) bool {
|
|
if len(value) != 17 {
|
|
return false
|
|
}
|
|
for _, character := range value {
|
|
if character < '0' || character > '9' {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
func validNotificationMessage(value string) bool {
|
|
if value == "" || len(value) > 200 || !utf8.ValidString(value) {
|
|
return false
|
|
}
|
|
for _, character := range value {
|
|
if character < 0x20 || character == 0x7f {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
func escapeUE4SSChatMessage(value string) string {
|
|
return strings.NewReplacer("\\", "\\\\", "\"", "\\\"").Replace(value)
|
|
}
|
|
func supportedVehicleSpawnCode(value string) bool {
|
|
return map[string]bool{"BPC_Laika_C": true, "BPC_WolfsWagen_C": true}[value]
|
|
}
|
|
func supportedConfigKey(key string) bool {
|
|
return map[string]bool{"ServerName": true, "GamePort": true, "QueryPort": true, "MaxPlayers": true, "WelcomeMessage": true}[key]
|
|
}
|
|
func redactConfigValues(values map[string]string) map[string]string {
|
|
result := map[string]string{}
|
|
keys := make([]string, 0, len(values))
|
|
for key := range values {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, key := range keys {
|
|
if supportedConfigKey(key) && safeDiagnosticField(key, values[key]) {
|
|
result[key] = values[key]
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
func safeDiagnosticField(key, value string) bool {
|
|
lowered := strings.ToLower(key + "=" + value)
|
|
return !strings.Contains(lowered, "path") && !strings.Contains(lowered, "credential") && !strings.Contains(lowered, "password") && !strings.Contains(lowered, "bearer ") && !strings.Contains(lowered, "rcon") && !strings.Contains(lowered, "sql") && !strings.Contains(lowered, "://")
|
|
}
|
|
func statePatchFields(raw []any) ([]StateFieldPatch, error) {
|
|
if len(raw) == 0 || len(raw) > 8 {
|
|
return nil, fmt.Errorf("state patch payload is invalid")
|
|
}
|
|
fields := make([]StateFieldPatch, 0, len(raw))
|
|
for _, value := range raw {
|
|
item, ok := value.(map[string]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("state patch payload is invalid")
|
|
}
|
|
key, _ := item["fieldKey"].(string)
|
|
before, beforeOK := item["before"].(float64)
|
|
after, afterOK := item["after"].(float64)
|
|
if key == "" || !beforeOK || !afterOK {
|
|
return nil, fmt.Errorf("state patch payload is invalid")
|
|
}
|
|
fields = append(fields, StateFieldPatch{Key: key, Before: before, After: after})
|
|
}
|
|
return fields, nil
|
|
}
|
|
func statePatchKeys(fields []StateFieldPatch) []string {
|
|
keys := make([]string, 0, len(fields))
|
|
for _, field := range fields {
|
|
keys = append(keys, field.Key)
|
|
}
|
|
return keys
|
|
}
|
|
func stateMatches(values map[string]float64, fields []StateFieldPatch) bool {
|
|
for _, field := range fields {
|
|
if values[field.Key] != field.Before {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
func stateApplied(values map[string]float64, fields []StateFieldPatch) bool {
|
|
for _, field := range fields {
|
|
if values[field.Key] != field.After {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
func rewardGrant(payload map[string]any) (RewardGrant, error) {
|
|
grantID, grantOK := payload["grantId"].(string)
|
|
playerID, playerOK := payload["playerId"].(string)
|
|
rawItems, itemsOK := payload["items"].([]any)
|
|
rawOperations, operationsOK := payload["operations"].([]any)
|
|
if !grantOK || !playerOK || !itemsOK || !operationsOK || (len(rawItems) == 0 && len(rawOperations) == 0) || len(rawItems) > 8 {
|
|
return RewardGrant{}, fmt.Errorf("reward payload is invalid")
|
|
}
|
|
items := make([]RewardItem, 0, len(rawItems))
|
|
for _, value := range rawItems {
|
|
item, ok := value.(map[string]any)
|
|
if !ok || len(item) != 2 {
|
|
return RewardGrant{}, fmt.Errorf("reward payload is invalid")
|
|
}
|
|
code, codeOK := item["catalogCode"].(string)
|
|
quantity, quantityOK := integerPayloadValue(item["quantity"])
|
|
if !codeOK || !supportedCatalogCode(code) || !quantityOK || quantity < 1 || quantity > 100 {
|
|
return RewardGrant{}, fmt.Errorf("reward payload is invalid")
|
|
}
|
|
items = append(items, RewardItem{CatalogCode: code, Quantity: quantity})
|
|
}
|
|
operations := make([]string, 0, len(rawOperations))
|
|
for _, value := range rawOperations {
|
|
operation, ok := value.(string)
|
|
if !ok || operation == "" || !utf8.ValidString(operation) {
|
|
return RewardGrant{}, fmt.Errorf("reward payload is invalid")
|
|
}
|
|
operations = append(operations, operation)
|
|
}
|
|
return RewardGrant{GrantID: grantID, PlayerID: playerID, Items: items, Operations: operations}, nil
|
|
}
|
|
|
|
func eventStartRequest(payload map[string]any) (EventStartRequest, error) {
|
|
eventID, eventIDOK := payload["eventId"].(string)
|
|
eventType, eventTypeOK := payload["eventType"].(string)
|
|
eventClass, classOK := integerPayloadValue(payload["class"])
|
|
title, titleOK := payload["title"].(string)
|
|
duration, durationOK := integerPayloadValue(payload["durationSeconds"])
|
|
percent, percentOK := integerPayloadValue(payload["percent"])
|
|
placard, placardOK := payload["placard"].(string)
|
|
produces, producesOK := eventProduceRequests(payload["produces"])
|
|
counts := make([]int, 4)
|
|
for index, key := range []string{"npc", "item", "zombie", "animal"} {
|
|
if value, exists := payload[key]; exists {
|
|
count, ok := integerPayloadValue(value)
|
|
if !ok || count < 0 || count > 10000 {
|
|
return EventStartRequest{}, fmt.Errorf("event start payload is invalid")
|
|
}
|
|
counts[index] = count
|
|
}
|
|
}
|
|
participants := 0
|
|
if value, exists := payload["maxParticipants"]; exists {
|
|
var ok bool
|
|
participants, ok = integerPayloadValue(value)
|
|
if !ok || participants < 1 || participants > 1000 {
|
|
return EventStartRequest{}, fmt.Errorf("event start payload is invalid")
|
|
}
|
|
}
|
|
announce := false
|
|
if value, exists := payload["announce"]; exists {
|
|
var ok bool
|
|
announce, ok = value.(bool)
|
|
if !ok {
|
|
return EventStartRequest{}, fmt.Errorf("event start payload is invalid")
|
|
}
|
|
}
|
|
if !eventIDOK || !eventTypeOK || !classOK || !titleOK || !durationOK || !percentOK || !placardOK || !producesOK || !supportedEventType(eventType) || eventClass < 1 || eventClass > 2 || (eventClass == 1) != (eventType == "range") || strings.TrimSpace(eventID) == "" || strings.TrimSpace(title) == "" || len(placard) > 500 || percent < 0 || percent > 100 || duration < 30 || duration > 86400 {
|
|
return EventStartRequest{}, fmt.Errorf("event start payload is invalid")
|
|
}
|
|
return EventStartRequest{EventID: eventID, EventType: eventType, Class: eventClass, Title: title, Placard: placard, Percent: percent, NPC: counts[0], Item: counts[1], Zombie: counts[2], Animal: counts[3], Produces: produces, DurationSeconds: duration, MaxParticipants: participants, Announce: announce}, nil
|
|
}
|
|
|
|
func eventProduceRequests(value any) ([]EventProduceRequest, bool) {
|
|
raw, ok := value.([]any)
|
|
if !ok || len(raw) > 100 {
|
|
return nil, false
|
|
}
|
|
result := make([]EventProduceRequest, 0, len(raw))
|
|
for _, candidate := range raw {
|
|
produce, ok := candidate.(map[string]any)
|
|
if !ok || len(produce) != 7 {
|
|
return nil, false
|
|
}
|
|
tradeGoodsID, idOK := produce["tradeGoodsId"].(string)
|
|
percent, percentOK := integerPayloadValue(produce["percent"])
|
|
quantity, quantityOK := integerPayloadValue(produce["value"])
|
|
radius, radiusOK := numberPayloadValue(produce["r"])
|
|
x, xOK := numberPayloadValue(produce["x"])
|
|
y, yOK := numberPayloadValue(produce["y"])
|
|
z, zOK := numberPayloadValue(produce["z"])
|
|
if !idOK || strings.TrimSpace(tradeGoodsID) == "" || len(tradeGoodsID) > 128 || !percentOK || percent < 0 || percent > 100 || !quantityOK || quantity < 1 || quantity > 10000 || !radiusOK || radius < 0 || radius > 2000000 || !xOK || !yOK || !zOK || x < -2000000 || x > 2000000 || y < -2000000 || y > 2000000 || z < -2000000 || z > 2000000 {
|
|
return nil, false
|
|
}
|
|
result = append(result, EventProduceRequest{TradeGoodsID: tradeGoodsID, Percent: percent, Value: quantity, Radius: radius, X: x, Y: y, Z: z})
|
|
}
|
|
return result, true
|
|
}
|
|
|
|
func integerPayloadValue(value any) (int, bool) {
|
|
switch number := value.(type) {
|
|
case float64:
|
|
if number != float64(int(number)) {
|
|
return 0, false
|
|
}
|
|
return int(number), true
|
|
case int:
|
|
return number, true
|
|
case int32:
|
|
return int(number), true
|
|
case int64:
|
|
return int(number), true
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
func numberPayloadValue(value any) (float64, bool) {
|
|
switch number := value.(type) {
|
|
case float64:
|
|
return number, true
|
|
case float32:
|
|
return float64(number), true
|
|
case int:
|
|
return float64(number), true
|
|
case int32:
|
|
return float64(number), true
|
|
case int64:
|
|
return float64(number), true
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
func supportedCatalogCode(value string) bool {
|
|
if value == "" || len(value) > 128 {
|
|
return false
|
|
}
|
|
for _, character := range value {
|
|
if (character < 'a' || character > 'z') && (character < 'A' || character > 'Z') && (character < '0' || character > '9') && character != '_' && character != '-' && character != '.' {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func supportedEventType(value string) bool {
|
|
return value == "range" || value == "fixed"
|
|
}
|