From dbed00f1e936035eaab2db1ab55cab2903a17833 Mon Sep 17 00:00:00 2001 From: edmand46 Date: Thu, 24 Sep 2026 17:59:23 +0300 Subject: [PATCH] chore: release v0.4.1 --- abi/abi_gen.go | 1190 ++++++++++++++++++++++++++++++++++++++++++ abi/data_hash_gen.go | 29 + data.go | 403 ++++++++++++++ go.mod | 3 + native.go | 17 + operations.go | 115 ++++ rudder.go | 179 +++++++ schema.go | 232 ++++++++ wasm.go | 46 ++ 9 files changed, 2214 insertions(+) create mode 100644 abi/abi_gen.go create mode 100644 abi/data_hash_gen.go create mode 100644 data.go create mode 100644 go.mod create mode 100644 native.go create mode 100644 operations.go create mode 100644 rudder.go create mode 100644 schema.go create mode 100644 wasm.go diff --git a/abi/abi_gen.go b/abi/abi_gen.go new file mode 100644 index 0000000..38697db --- /dev/null +++ b/abi/abi_gen.go @@ -0,0 +1,1190 @@ +// Code generated by apigen. DO NOT EDIT. + +package abi + +import "encoding/json" + +const ABIVersion = "rudder_v1" + +const ( + OpCatalogActive = "catalog.active" + OpCatalogGet = "catalog.get" + OpCatalogList = "catalog.list" + OpCounterAdd = "counter.add" + OpCounterAggregate = "counter.aggregate" + OpCounterGet = "counter.get" + OpCounterList = "counter.list" + OpCounterSet = "counter.set" + OpCounterWindows = "counter.windows" + OpDataExec = "data.exec" + OpDocDelete = "doc.delete" + OpDocGet = "doc.get" + OpDocList = "doc.list" + OpDocPut = "doc.put" + OpDocUpsert = "doc.upsert" + OpEventsEmit = "events.emit" + OpInventoryConsume = "inventory.consume" + OpInventoryGet = "inventory.get" + OpInventoryGrant = "inventory.grant" + OpInventoryHas = "inventory.has" + OpInventoryList = "inventory.list" + OpLedgerBalance = "ledger.balance" + OpLedgerBalances = "ledger.balances" + OpLedgerCredit = "ledger.credit" + OpLedgerDebit = "ledger.debit" + OpLedgerHistory = "ledger.history" + OpModulesCall = "modules.call" + OpProjectStorageGet = "projectStorage.get" + OpProjectStorageSet = "projectStorage.set" + OpQueueLength = "queue.length" + OpQueuePeek = "queue.peek" + OpQueuePop = "queue.pop" + OpQueuePush = "queue.push" + OpQueueRemove = "queue.remove" + OpRegenFill = "regen.fill" + OpRegenGet = "regen.get" + OpRegenRefill = "regen.refill" + OpRegenSpend = "regen.spend" + OpRewardGrant = "reward.grant" + OpSegmentsBucket = "segments.bucket" + OpSegmentsMatch = "segments.match" + OpSortedSetAmong = "sortedSet.among" + OpSortedSetAround = "sortedSet.around" + OpSortedSetInfo = "sortedSet.info" + OpSortedSetPage = "sortedSet.page" + OpSortedSetPrevious = "sortedSet.previous" + OpSortedSetRank = "sortedSet.rank" + OpSortedSetRemove = "sortedSet.remove" + OpSortedSetReset = "sortedSet.reset" + OpSortedSetSubmit = "sortedSet.submit" + OpStorageGet = "storage.get" + OpStorageSet = "storage.set" + OpTimerArm = "timer.arm" + OpTimerDisarm = "timer.disarm" + OpTimerPeek = "timer.peek" +) + +const ( + ErrorCodeCallDepthExceeded = "call_depth_exceeded" + ErrorCodeCatalogInvalidField = "catalog_invalid_field" + ErrorCodeCatalogNotFound = "catalog_not_found" + ErrorCodeCatalogRefInvalid = "catalog_ref_invalid" + ErrorCodeCounterAboveMax = "counter_above_max" + ErrorCodeCounterBelowMin = "counter_below_min" + ErrorCodeCurrencyUnknown = "currency_unknown" + ErrorCodeDependencyNotDeclared = "dependency_not_declared" + ErrorCodeFunctionNotCallable = "function_not_callable" + ErrorCodeFunctionNotFound = "function_not_found" + ErrorCodeInsufficientFunds = "insufficient_funds" + ErrorCodeInsufficientItems = "insufficient_items" + ErrorCodeInternal = "internal" + ErrorCodeInvalidParameters = "invalid_parameters" + ErrorCodeModuleNotInstalled = "module_not_installed" + ErrorCodePlayerNotFound = "player_not_found" + ErrorCodePlayerRequired = "player_required" + ErrorCodePrimitiveNotDeclared = "primitive_not_declared" + ErrorCodeRegenInsufficient = "regen_insufficient" + ErrorCodeReservedEvent = "reserved_event" + ErrorCodeSortedSetOrderChanged = "sorted_set_order_changed" + ErrorCodeStorageKeyLimitExceeded = "storage_key_limit_exceeded" + ErrorCodeUnknownOperation = "unknown_operation" + ErrorCodeVersionConflict = "version_conflict" +) + +var ErrorCodes = []string{ + ErrorCodeCallDepthExceeded, + ErrorCodeCatalogInvalidField, + ErrorCodeCatalogNotFound, + ErrorCodeCatalogRefInvalid, + ErrorCodeCounterAboveMax, + ErrorCodeCounterBelowMin, + ErrorCodeCurrencyUnknown, + ErrorCodeDependencyNotDeclared, + ErrorCodeFunctionNotCallable, + ErrorCodeFunctionNotFound, + ErrorCodeInsufficientFunds, + ErrorCodeInsufficientItems, + ErrorCodeInternal, + ErrorCodeInvalidParameters, + ErrorCodeModuleNotInstalled, + ErrorCodePlayerNotFound, + ErrorCodePlayerRequired, + ErrorCodePrimitiveNotDeclared, + ErrorCodeRegenInsufficient, + ErrorCodeReservedEvent, + ErrorCodeSortedSetOrderChanged, + ErrorCodeStorageKeyLimitExceeded, + ErrorCodeUnknownOperation, + ErrorCodeVersionConflict, +} + +type Amount struct { + Amount int64 `json:"amount"` + Currency string `json:"currency"` +} + +type CallResponse struct { + Error *ErrorBody `json:"error,omitempty"` + Result json.RawMessage `json:"result,omitempty"` +} + +type Context struct { + Config json.RawMessage `json:"config"` + Environment string `json:"environment"` + Module string `json:"module"` + Now string `json:"now"` + Player *Player `json:"player"` + Trigger string `json:"trigger"` +} + +type CounterEntry struct { + Key string `json:"key"` + Value int64 `json:"value"` +} + +type CounterWindow struct { + UpdatedAt string `json:"updatedAt"` + Value int64 `json:"value"` + Window string `json:"window"` +} + +type Describe struct { + DataHash string `json:"dataHash,omitempty"` + Functions []DescribedFunction `json:"functions"` +} + +type DescribedFunction struct { + Errors []string `json:"errors"` + Input json.RawMessage `json:"input"` + Name string `json:"name"` + Output json.RawMessage `json:"output"` +} + +type DocEntry struct { + Key string `json:"key"` + UpdatedAt string `json:"updatedAt"` + Value json.RawMessage `json:"value"` + Version int64 `json:"version"` +} + +type ErrorBody struct { + Code string `json:"code"` + Message string `json:"message"` +} + +type Input struct { + Arguments json.RawMessage `json:"args"` + Context Context `json:"context"` + Function string `json:"fn"` +} + +type LedgerEntry struct { + Amount int64 `json:"amount"` + BalanceAfter int64 `json:"balanceAfter"` + CreatedAt string `json:"createdAt"` + Currency string `json:"currency"` + ID string `json:"id"` + Reason string `json:"reason"` +} + +type Output struct { + Error *ErrorBody `json:"error,omitempty"` + Result json.RawMessage `json:"result,omitempty"` +} + +type Owner struct { + ID string `json:"id"` + Kind string `json:"kind"` +} + +type Player struct { + ID string `json:"id"` +} + +type QueueItem struct { + Data json.RawMessage `json:"data"` + ReadyAt *string `json:"readyAt"` + Seq int64 `json:"seq"` +} + +type SortedSetEntry struct { + AvatarURL string `json:"avatarUrl,omitempty"` + Nickname string `json:"nickname"` + Owner Owner `json:"owner"` + Rank int64 `json:"rank"` + Score int64 `json:"score"` + UpdatedAt string `json:"updatedAt"` +} + +type Stack struct { + Item string `json:"item"` + Quantity int64 `json:"quantity"` +} + +type StorageItem struct { + Data json.RawMessage `json:"data"` + Type string `json:"type"` + UpdatedAt string `json:"updatedAt"` + Version int64 `json:"version"` +} + +type CatalogActiveParameters struct { + Op string `json:"op"` + Eq json.RawMessage `json:"eq,omitempty"` + Field string `json:"field,omitempty"` + Limit int64 `json:"limit"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` +} + +func (parameters CatalogActiveParameters) MarshalJSON() ([]byte, error) { + type plain CatalogActiveParameters + parameters.Op = OpCatalogActive + return json.Marshal(plain(parameters)) +} + +type CatalogActiveResult struct { + Rows []json.RawMessage `json:"rows"` + Truncated bool `json:"truncated"` +} + +type CatalogGetParameters struct { + Op string `json:"op"` + Eq json.RawMessage `json:"eq"` + Field string `json:"field"` + Primitive string `json:"primitive"` +} + +func (parameters CatalogGetParameters) MarshalJSON() ([]byte, error) { + type plain CatalogGetParameters + parameters.Op = OpCatalogGet + return json.Marshal(plain(parameters)) +} + +type CatalogGetResult struct { + Found bool `json:"found"` + Row json.RawMessage `json:"row"` +} + +type CatalogListParameters struct { + Op string `json:"op"` + Eq json.RawMessage `json:"eq,omitempty"` + Field string `json:"field,omitempty"` + Limit int64 `json:"limit"` + Primitive string `json:"primitive"` +} + +func (parameters CatalogListParameters) MarshalJSON() ([]byte, error) { + type plain CatalogListParameters + parameters.Op = OpCatalogList + return json.Marshal(plain(parameters)) +} + +type CatalogListResult struct { + Rows []json.RawMessage `json:"rows"` + Truncated bool `json:"truncated"` +} + +type CounterAddParameters struct { + Op string `json:"op"` + Delta int64 `json:"delta"` + Key string `json:"key"` + OpKey string `json:"opKey,omitempty"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` +} + +func (parameters CounterAddParameters) MarshalJSON() ([]byte, error) { + type plain CounterAddParameters + parameters.Op = OpCounterAdd + return json.Marshal(plain(parameters)) +} + +type CounterAddResult struct { + Value int64 `json:"value"` + Window string `json:"window"` +} + +type CounterAggregateParameters struct { + Op string `json:"op"` + Key string `json:"key,omitempty"` + Primitive string `json:"primitive"` +} + +func (parameters CounterAggregateParameters) MarshalJSON() ([]byte, error) { + type plain CounterAggregateParameters + parameters.Op = OpCounterAggregate + return json.Marshal(plain(parameters)) +} + +type CounterAggregateResult struct { + Owners int64 `json:"owners"` + Value int64 `json:"value"` +} + +type CounterGetParameters struct { + Op string `json:"op"` + Key string `json:"key"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` +} + +func (parameters CounterGetParameters) MarshalJSON() ([]byte, error) { + type plain CounterGetParameters + parameters.Op = OpCounterGet + return json.Marshal(plain(parameters)) +} + +type CounterGetResult struct { + Value int64 `json:"value"` + Window string `json:"window"` +} + +type CounterListParameters struct { + Op string `json:"op"` + Limit int64 `json:"limit"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` +} + +func (parameters CounterListParameters) MarshalJSON() ([]byte, error) { + type plain CounterListParameters + parameters.Op = OpCounterList + return json.Marshal(plain(parameters)) +} + +type CounterListResult struct { + Entries []CounterEntry `json:"entries"` +} + +type CounterSetParameters struct { + Op string `json:"op"` + Key string `json:"key"` + OpKey string `json:"opKey,omitempty"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` + Value int64 `json:"value"` +} + +func (parameters CounterSetParameters) MarshalJSON() ([]byte, error) { + type plain CounterSetParameters + parameters.Op = OpCounterSet + return json.Marshal(plain(parameters)) +} + +type CounterSetResult struct { + Value int64 `json:"value"` + Window string `json:"window"` +} + +type CounterWindowsParameters struct { + Op string `json:"op"` + Key string `json:"key"` + Last int64 `json:"last"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` +} + +func (parameters CounterWindowsParameters) MarshalJSON() ([]byte, error) { + type plain CounterWindowsParameters + parameters.Op = OpCounterWindows + return json.Marshal(plain(parameters)) +} + +type CounterWindowsResult struct { + Entries []CounterWindow `json:"entries"` +} + +type DataExecParameters struct { + Op string `json:"op"` + Commands []json.RawMessage `json:"commands"` +} + +func (parameters DataExecParameters) MarshalJSON() ([]byte, error) { + type plain DataExecParameters + parameters.Op = OpDataExec + return json.Marshal(plain(parameters)) +} + +type DataExecResult struct { + Results []json.RawMessage `json:"results"` +} + +type DocDeleteParameters struct { + Op string `json:"op"` + Key string `json:"key"` + OpKey string `json:"opKey,omitempty"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` +} + +func (parameters DocDeleteParameters) MarshalJSON() ([]byte, error) { + type plain DocDeleteParameters + parameters.Op = OpDocDelete + return json.Marshal(plain(parameters)) +} + +type DocGetParameters struct { + Op string `json:"op"` + Key string `json:"key"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` +} + +func (parameters DocGetParameters) MarshalJSON() ([]byte, error) { + type plain DocGetParameters + parameters.Op = OpDocGet + return json.Marshal(plain(parameters)) +} + +type DocGetResult struct { + Found bool `json:"found"` + Key string `json:"key"` + UpdatedAt string `json:"updatedAt"` + Value json.RawMessage `json:"value"` + Version int64 `json:"version"` +} + +type DocListParameters struct { + Op string `json:"op"` + Limit int64 `json:"limit"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` +} + +func (parameters DocListParameters) MarshalJSON() ([]byte, error) { + type plain DocListParameters + parameters.Op = OpDocList + return json.Marshal(plain(parameters)) +} + +type DocListResult struct { + Entries []DocEntry `json:"entries"` +} + +type DocPutParameters struct { + Op string `json:"op"` + Data json.RawMessage `json:"data"` + Key string `json:"key"` + OpKey string `json:"opKey,omitempty"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` + Version int64 `json:"version"` +} + +func (parameters DocPutParameters) MarshalJSON() ([]byte, error) { + type plain DocPutParameters + parameters.Op = OpDocPut + return json.Marshal(plain(parameters)) +} + +type DocPutResult struct { + Version int64 `json:"version"` +} + +type DocUpsertParameters struct { + Op string `json:"op"` + Data json.RawMessage `json:"data"` + Key string `json:"key"` + OpKey string `json:"opKey,omitempty"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` +} + +func (parameters DocUpsertParameters) MarshalJSON() ([]byte, error) { + type plain DocUpsertParameters + parameters.Op = OpDocUpsert + return json.Marshal(plain(parameters)) +} + +type DocUpsertResult struct { + Version int64 `json:"version"` +} + +type EventsEmitParameters struct { + Op string `json:"op"` + Name string `json:"name"` + Payload json.RawMessage `json:"payload,omitempty"` +} + +func (parameters EventsEmitParameters) MarshalJSON() ([]byte, error) { + type plain EventsEmitParameters + parameters.Op = OpEventsEmit + return json.Marshal(plain(parameters)) +} + +type InventoryConsumeParameters struct { + Op string `json:"op"` + Items []Stack `json:"items"` + OpKey string `json:"opKey,omitempty"` + Owner Owner `json:"owner"` +} + +func (parameters InventoryConsumeParameters) MarshalJSON() ([]byte, error) { + type plain InventoryConsumeParameters + parameters.Op = OpInventoryConsume + return json.Marshal(plain(parameters)) +} + +type InventoryGetParameters struct { + Op string `json:"op"` + Item string `json:"item"` + Owner Owner `json:"owner"` +} + +func (parameters InventoryGetParameters) MarshalJSON() ([]byte, error) { + type plain InventoryGetParameters + parameters.Op = OpInventoryGet + return json.Marshal(plain(parameters)) +} + +type InventoryGetResult struct { + Quantity int64 `json:"quantity"` +} + +type InventoryGrantParameters struct { + Op string `json:"op"` + Items []Stack `json:"items"` + OpKey string `json:"opKey,omitempty"` + Owner Owner `json:"owner"` +} + +func (parameters InventoryGrantParameters) MarshalJSON() ([]byte, error) { + type plain InventoryGrantParameters + parameters.Op = OpInventoryGrant + return json.Marshal(plain(parameters)) +} + +type InventoryHasParameters struct { + Op string `json:"op"` + Items []Stack `json:"items"` + Owner Owner `json:"owner"` +} + +func (parameters InventoryHasParameters) MarshalJSON() ([]byte, error) { + type plain InventoryHasParameters + parameters.Op = OpInventoryHas + return json.Marshal(plain(parameters)) +} + +type InventoryHasResult struct { + Holds bool `json:"holds"` +} + +type InventoryListParameters struct { + Op string `json:"op"` + Owner Owner `json:"owner"` +} + +func (parameters InventoryListParameters) MarshalJSON() ([]byte, error) { + type plain InventoryListParameters + parameters.Op = OpInventoryList + return json.Marshal(plain(parameters)) +} + +type InventoryListResult struct { + Items []Stack `json:"items"` +} + +type LedgerBalanceParameters struct { + Op string `json:"op"` + Currency string `json:"currency"` + Owner Owner `json:"owner"` +} + +func (parameters LedgerBalanceParameters) MarshalJSON() ([]byte, error) { + type plain LedgerBalanceParameters + parameters.Op = OpLedgerBalance + return json.Marshal(plain(parameters)) +} + +type LedgerBalanceResult struct { + Balance int64 `json:"balance"` +} + +type LedgerBalancesParameters struct { + Op string `json:"op"` + Owner Owner `json:"owner"` +} + +func (parameters LedgerBalancesParameters) MarshalJSON() ([]byte, error) { + type plain LedgerBalancesParameters + parameters.Op = OpLedgerBalances + return json.Marshal(plain(parameters)) +} + +type LedgerBalancesResult struct { + Accounts []Amount `json:"accounts"` +} + +type LedgerCreditParameters struct { + Op string `json:"op"` + Amount int64 `json:"amount"` + Currency string `json:"currency"` + OpKey string `json:"opKey,omitempty"` + Owner Owner `json:"owner"` + Reason string `json:"reason"` +} + +func (parameters LedgerCreditParameters) MarshalJSON() ([]byte, error) { + type plain LedgerCreditParameters + parameters.Op = OpLedgerCredit + return json.Marshal(plain(parameters)) +} + +type LedgerCreditResult struct { + Balance int64 `json:"balance"` +} + +type LedgerDebitParameters struct { + Op string `json:"op"` + Amount int64 `json:"amount"` + Currency string `json:"currency"` + OpKey string `json:"opKey,omitempty"` + Owner Owner `json:"owner"` + Reason string `json:"reason"` +} + +func (parameters LedgerDebitParameters) MarshalJSON() ([]byte, error) { + type plain LedgerDebitParameters + parameters.Op = OpLedgerDebit + return json.Marshal(plain(parameters)) +} + +type LedgerDebitResult struct { + Balance int64 `json:"balance"` +} + +type LedgerHistoryParameters struct { + Op string `json:"op"` + Currency string `json:"currency,omitempty"` + Limit int64 `json:"limit"` + Owner Owner `json:"owner"` +} + +func (parameters LedgerHistoryParameters) MarshalJSON() ([]byte, error) { + type plain LedgerHistoryParameters + parameters.Op = OpLedgerHistory + return json.Marshal(plain(parameters)) +} + +type LedgerHistoryResult struct { + Entries []LedgerEntry `json:"entries"` +} + +type ModulesCallParameters struct { + Op string `json:"op"` + Arguments json.RawMessage `json:"args,omitempty"` + Function string `json:"fn"` + Module string `json:"module"` +} + +func (parameters ModulesCallParameters) MarshalJSON() ([]byte, error) { + type plain ModulesCallParameters + parameters.Op = OpModulesCall + return json.Marshal(plain(parameters)) +} + +type ModulesCallResult struct { + Value json.RawMessage `json:"value"` +} + +type ProjectStorageGetParameters struct { + Op string `json:"op"` + Type string `json:"type"` +} + +func (parameters ProjectStorageGetParameters) MarshalJSON() ([]byte, error) { + type plain ProjectStorageGetParameters + parameters.Op = OpProjectStorageGet + return json.Marshal(plain(parameters)) +} + +type ProjectStorageGetResult struct { + Item *StorageItem `json:"item"` +} + +type ProjectStorageSetParameters struct { + Op string `json:"op"` + Data json.RawMessage `json:"data"` + OpKey string `json:"opKey,omitempty"` + Type string `json:"type"` +} + +func (parameters ProjectStorageSetParameters) MarshalJSON() ([]byte, error) { + type plain ProjectStorageSetParameters + parameters.Op = OpProjectStorageSet + return json.Marshal(plain(parameters)) +} + +type QueueLengthParameters struct { + Op string `json:"op"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` +} + +func (parameters QueueLengthParameters) MarshalJSON() ([]byte, error) { + type plain QueueLengthParameters + parameters.Op = OpQueueLength + return json.Marshal(plain(parameters)) +} + +type QueueLengthResult struct { + Length int64 `json:"length"` + Ready int64 `json:"ready"` +} + +type QueuePeekParameters struct { + Op string `json:"op"` + Limit int64 `json:"limit"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` +} + +func (parameters QueuePeekParameters) MarshalJSON() ([]byte, error) { + type plain QueuePeekParameters + parameters.Op = OpQueuePeek + return json.Marshal(plain(parameters)) +} + +type QueuePeekResult struct { + Items []QueueItem `json:"items"` +} + +type QueuePopParameters struct { + Op string `json:"op"` + OpKey string `json:"opKey,omitempty"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` +} + +func (parameters QueuePopParameters) MarshalJSON() ([]byte, error) { + type plain QueuePopParameters + parameters.Op = OpQueuePop + return json.Marshal(plain(parameters)) +} + +type QueuePopResult struct { + Item *QueueItem `json:"item"` +} + +type QueuePushParameters struct { + Op string `json:"op"` + Data json.RawMessage `json:"data"` + OpKey string `json:"opKey,omitempty"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` + ReadyAt *string `json:"readyAt,omitempty"` +} + +func (parameters QueuePushParameters) MarshalJSON() ([]byte, error) { + type plain QueuePushParameters + parameters.Op = OpQueuePush + return json.Marshal(plain(parameters)) +} + +type QueuePushResult struct { + Seq int64 `json:"seq"` +} + +type QueueRemoveParameters struct { + Op string `json:"op"` + OpKey string `json:"opKey,omitempty"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` + Seq int64 `json:"seq"` +} + +func (parameters QueueRemoveParameters) MarshalJSON() ([]byte, error) { + type plain QueueRemoveParameters + parameters.Op = OpQueueRemove + return json.Marshal(plain(parameters)) +} + +type QueueRemoveResult struct { + Removed bool `json:"removed"` +} + +type RegenFillParameters struct { + Op string `json:"op"` + Key string `json:"key"` + OpKey string `json:"opKey,omitempty"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` +} + +func (parameters RegenFillParameters) MarshalJSON() ([]byte, error) { + type plain RegenFillParameters + parameters.Op = OpRegenFill + return json.Marshal(plain(parameters)) +} + +type RegenFillResult struct { + Cap int64 `json:"cap"` + FullAt *string `json:"fullAt"` + NextAt *string `json:"nextAt"` + Value int64 `json:"value"` +} + +type RegenGetParameters struct { + Op string `json:"op"` + Key string `json:"key"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` +} + +func (parameters RegenGetParameters) MarshalJSON() ([]byte, error) { + type plain RegenGetParameters + parameters.Op = OpRegenGet + return json.Marshal(plain(parameters)) +} + +type RegenGetResult struct { + Cap int64 `json:"cap"` + FullAt *string `json:"fullAt"` + NextAt *string `json:"nextAt"` + Value int64 `json:"value"` +} + +type RegenRefillParameters struct { + Op string `json:"op"` + Amount int64 `json:"amount"` + Key string `json:"key"` + OpKey string `json:"opKey,omitempty"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` +} + +func (parameters RegenRefillParameters) MarshalJSON() ([]byte, error) { + type plain RegenRefillParameters + parameters.Op = OpRegenRefill + return json.Marshal(plain(parameters)) +} + +type RegenRefillResult struct { + Cap int64 `json:"cap"` + FullAt *string `json:"fullAt"` + NextAt *string `json:"nextAt"` + Value int64 `json:"value"` +} + +type RegenSpendParameters struct { + Op string `json:"op"` + Amount int64 `json:"amount"` + Key string `json:"key"` + OpKey string `json:"opKey,omitempty"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` +} + +func (parameters RegenSpendParameters) MarshalJSON() ([]byte, error) { + type plain RegenSpendParameters + parameters.Op = OpRegenSpend + return json.Marshal(plain(parameters)) +} + +type RegenSpendResult struct { + Cap int64 `json:"cap"` + FullAt *string `json:"fullAt"` + NextAt *string `json:"nextAt"` + Value int64 `json:"value"` +} + +type RewardGrantParameters struct { + Op string `json:"op"` + Currencies []Amount `json:"currencies,omitempty"` + Items []Stack `json:"items,omitempty"` + OpKey string `json:"opKey,omitempty"` + Owner Owner `json:"owner"` + Reason string `json:"reason,omitempty"` +} + +func (parameters RewardGrantParameters) MarshalJSON() ([]byte, error) { + type plain RewardGrantParameters + parameters.Op = OpRewardGrant + return json.Marshal(plain(parameters)) +} + +type SegmentsBucketParameters struct { + Op string `json:"op"` + PlayerID string `json:"playerId"` + Salt string `json:"salt"` +} + +func (parameters SegmentsBucketParameters) MarshalJSON() ([]byte, error) { + type plain SegmentsBucketParameters + parameters.Op = OpSegmentsBucket + return json.Marshal(plain(parameters)) +} + +type SegmentsBucketResult struct { + Bucket int64 `json:"bucket"` +} + +type SegmentsMatchParameters struct { + Op string `json:"op"` + PlayerID string `json:"playerId"` + Segment json.RawMessage `json:"segment"` +} + +func (parameters SegmentsMatchParameters) MarshalJSON() ([]byte, error) { + type plain SegmentsMatchParameters + parameters.Op = OpSegmentsMatch + return json.Marshal(plain(parameters)) +} + +type SegmentsMatchResult struct { + Match bool `json:"match"` +} + +type SortedSetAmongParameters struct { + Op string `json:"op"` + Key string `json:"key"` + Owners []Owner `json:"owners"` + Primitive string `json:"primitive"` +} + +func (parameters SortedSetAmongParameters) MarshalJSON() ([]byte, error) { + type plain SortedSetAmongParameters + parameters.Op = OpSortedSetAmong + return json.Marshal(plain(parameters)) +} + +type SortedSetAmongResult struct { + Entries []SortedSetEntry `json:"entries"` + Total int64 `json:"total"` +} + +type SortedSetAroundParameters struct { + Op string `json:"op"` + After int64 `json:"after"` + Before int64 `json:"before"` + Key string `json:"key"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` +} + +func (parameters SortedSetAroundParameters) MarshalJSON() ([]byte, error) { + type plain SortedSetAroundParameters + parameters.Op = OpSortedSetAround + return json.Marshal(plain(parameters)) +} + +type SortedSetAroundResult struct { + Entries []SortedSetEntry `json:"entries"` + Total int64 `json:"total"` +} + +type SortedSetInfoParameters struct { + Op string `json:"op"` + Key string `json:"key"` + Primitive string `json:"primitive"` +} + +func (parameters SortedSetInfoParameters) MarshalJSON() ([]byte, error) { + type plain SortedSetInfoParameters + parameters.Op = OpSortedSetInfo + return json.Marshal(plain(parameters)) +} + +type SortedSetInfoResult struct { + Archived []string `json:"archived"` + Entries int64 `json:"entries"` + LastResetAt *string `json:"lastResetAt"` + MaxEntries int64 `json:"maxEntries"` + NextResetAt *string `json:"nextResetAt"` + Order string `json:"order"` + Pending bool `json:"pending"` + Period string `json:"period"` + PeriodStart string `json:"periodStart"` +} + +type SortedSetPageParameters struct { + Op string `json:"op"` + Key string `json:"key"` + Limit int64 `json:"limit"` + Offset int64 `json:"offset"` + Primitive string `json:"primitive"` +} + +func (parameters SortedSetPageParameters) MarshalJSON() ([]byte, error) { + type plain SortedSetPageParameters + parameters.Op = OpSortedSetPage + return json.Marshal(plain(parameters)) +} + +type SortedSetPageResult struct { + Entries []SortedSetEntry `json:"entries"` + Total int64 `json:"total"` +} + +type SortedSetPreviousParameters struct { + Op string `json:"op"` + Back int64 `json:"back"` + Key string `json:"key"` + Limit int64 `json:"limit"` + Primitive string `json:"primitive"` +} + +func (parameters SortedSetPreviousParameters) MarshalJSON() ([]byte, error) { + type plain SortedSetPreviousParameters + parameters.Op = OpSortedSetPrevious + return json.Marshal(plain(parameters)) +} + +type SortedSetPreviousResult struct { + Entries []SortedSetEntry `json:"entries"` + PeriodEnd string `json:"periodEnd"` + Total int64 `json:"total"` +} + +type SortedSetRankParameters struct { + Op string `json:"op"` + Key string `json:"key"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` +} + +func (parameters SortedSetRankParameters) MarshalJSON() ([]byte, error) { + type plain SortedSetRankParameters + parameters.Op = OpSortedSetRank + return json.Marshal(plain(parameters)) +} + +type SortedSetRankResult struct { + Entry SortedSetEntry `json:"entry"` + Found bool `json:"found"` + Ranked bool `json:"ranked"` + Total int64 `json:"total"` +} + +type SortedSetRemoveParameters struct { + Op string `json:"op"` + Key string `json:"key"` + OpKey string `json:"opKey,omitempty"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` +} + +func (parameters SortedSetRemoveParameters) MarshalJSON() ([]byte, error) { + type plain SortedSetRemoveParameters + parameters.Op = OpSortedSetRemove + return json.Marshal(plain(parameters)) +} + +type SortedSetResetParameters struct { + Op string `json:"op"` + Key string `json:"key"` + OpKey string `json:"opKey,omitempty"` + Primitive string `json:"primitive"` +} + +func (parameters SortedSetResetParameters) MarshalJSON() ([]byte, error) { + type plain SortedSetResetParameters + parameters.Op = OpSortedSetReset + return json.Marshal(plain(parameters)) +} + +type SortedSetSubmitParameters struct { + Op string `json:"op"` + Key string `json:"key"` + OpKey string `json:"opKey,omitempty"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` + Score int64 `json:"score"` +} + +func (parameters SortedSetSubmitParameters) MarshalJSON() ([]byte, error) { + type plain SortedSetSubmitParameters + parameters.Op = OpSortedSetSubmit + return json.Marshal(plain(parameters)) +} + +type SortedSetSubmitResult struct { + Changed bool `json:"changed"` + Previous int64 `json:"previous"` + Score int64 `json:"score"` +} + +type StorageGetParameters struct { + Op string `json:"op"` + PlayerID string `json:"playerId"` + Type string `json:"type"` +} + +func (parameters StorageGetParameters) MarshalJSON() ([]byte, error) { + type plain StorageGetParameters + parameters.Op = OpStorageGet + return json.Marshal(plain(parameters)) +} + +type StorageGetResult struct { + Item *StorageItem `json:"item"` +} + +type StorageSetParameters struct { + Op string `json:"op"` + Data json.RawMessage `json:"data"` + OpKey string `json:"opKey,omitempty"` + PlayerID string `json:"playerId"` + Type string `json:"type"` +} + +func (parameters StorageSetParameters) MarshalJSON() ([]byte, error) { + type plain StorageSetParameters + parameters.Op = OpStorageSet + return json.Marshal(plain(parameters)) +} + +type TimerArmParameters struct { + Op string `json:"op"` + FireAt string `json:"fireAt"` + Key string `json:"key"` + OpKey string `json:"opKey,omitempty"` + Owner Owner `json:"owner"` + Payload json.RawMessage `json:"payload,omitempty"` + Primitive string `json:"primitive"` +} + +func (parameters TimerArmParameters) MarshalJSON() ([]byte, error) { + type plain TimerArmParameters + parameters.Op = OpTimerArm + return json.Marshal(plain(parameters)) +} + +type TimerDisarmParameters struct { + Op string `json:"op"` + Key string `json:"key"` + OpKey string `json:"opKey,omitempty"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` +} + +func (parameters TimerDisarmParameters) MarshalJSON() ([]byte, error) { + type plain TimerDisarmParameters + parameters.Op = OpTimerDisarm + return json.Marshal(plain(parameters)) +} + +type TimerDisarmResult struct { + Disarmed bool `json:"disarmed"` +} + +type TimerPeekParameters struct { + Op string `json:"op"` + Key string `json:"key"` + Owner Owner `json:"owner"` + Primitive string `json:"primitive"` +} + +func (parameters TimerPeekParameters) MarshalJSON() ([]byte, error) { + type plain TimerPeekParameters + parameters.Op = OpTimerPeek + return json.Marshal(plain(parameters)) +} + +type TimerPeekResult struct { + Armed bool `json:"armed"` + FireAt *string `json:"fireAt"` + Payload json.RawMessage `json:"payload"` +} diff --git a/abi/data_hash_gen.go b/abi/data_hash_gen.go new file mode 100644 index 0000000..ad79ecd --- /dev/null +++ b/abi/data_hash_gen.go @@ -0,0 +1,29 @@ +// Code generated by apigen. DO NOT EDIT. + +package abi + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" +) + +// DataHash is the canonical hash of a manifest primitives block. The generated data package bakes +// it in and an upload whose manifest does not match it is rejected, so a manifest edited without a +// regenerate never runs against stale generated types. Key order and whitespace do not matter. +// A module with no primitives hashes to the empty string. +func DataHash(primitives json.RawMessage) string { + var decoded any + if err := json.Unmarshal(primitives, &decoded); err != nil || decoded == nil { + return "" + } + + encoded, err := json.Marshal(decoded) + if err != nil || string(encoded) == "{}" { + return "" + } + + sum := sha256.Sum256(encoded) + + return hex.EncodeToString(sum[:]) +} diff --git a/data.go b/data.go new file mode 100644 index 0000000..66ca7c8 --- /dev/null +++ b/data.go @@ -0,0 +1,403 @@ +package rudder + +import ( + "encoding/json" + "errors" + "sort" + "time" + + "hub.rudder.build/rudder/rudder-module-go/abi" +) + +const ( + OwnerKindPlayer = "player" + OwnerKindProject = "project" + + catalogLimit = 1000 +) + +type Owner = abi.Owner + +func Player(id string) Owner { return Owner{Kind: OwnerKindPlayer, ID: id} } + +func (c *Context) Owner() Owner { + if c.Player == nil { + return Owner{} + } + return Owner{Kind: OwnerKindPlayer, ID: c.Player.ID} +} + +type Op struct{ Key string } + +func OpKey(op []Op) string { + if len(op) == 0 { + return "" + } + return op[0].Key +} + +// Reward is what a designer types into a catalog row: some currencies, some items. It is granted +// as ONE mutation, all-or-nothing: a reward that fails halfway leaves nothing behind, and one +// idempotency key makes the whole grant idempotent. +type Reward struct { + Currencies map[string]int64 `json:"currencies,omitempty"` + Items map[string]int64 `json:"items,omitempty"` +} + +func (r Reward) Empty() bool { + return len(r.Currencies) == 0 && len(r.Items) == 0 +} + +// Price is what a catalog field of type price carries. Without it a module hand-decodes a +// json.RawMessage every time it reads the number a designer typed into the price form. +type Price struct { + Currency string `json:"currency"` + Amount int64 `json:"amount"` +} + +// Version is a document's optimistic-concurrency token. Zero means the document does not exist, +// which is what Get reads back for a miss and what Put demands for "must not exist yet". +type Version int64 + +type Written struct { + Version Version `json:"version"` +} + +// Unchanged tells Patch not to write. Patch returns the document as it was, with no error. +var Unchanged = errors.New("rudder: document unchanged") + +// IsCode reports whether err is a kernel business error carrying this code. Without it every +// module writes the same errors.As block to map one kernel code onto one of its own. +func IsCode(err error, code string) bool { + var failure *Error + return errors.As(err, &failure) && failure.Code == code +} + +// JSON marshals a generated document type for a command. A type the generator emitted always +// marshals, so a failure here is a generator bug: it traps the call rather than letting the kernel +// store null. +func JSON(value any) json.RawMessage { + body, err := json.Marshal(value) + if err != nil { + panic("rudder: generated document does not marshal: " + err.Error()) + } + return body +} + +// Stamp is the wire form of an optional timestamp. The ABI carries times as RFC3339 strings, and a +// nil time has to stay nil rather than become the zero year. +func Stamp(value *time.Time) *string { + if value == nil { + return nil + } + formatted := value.UTC().Format(time.RFC3339Nano) + return &formatted +} + +func One[Result any](command any) (Result, error) { + var result Result + if err := call(command, &result); err != nil { + var zero Result + return zero, err + } + return result, nil +} + +type Batch struct { + commands []json.RawMessage + targets []any + ran bool +} + +func (b *Batch) add(command any, target any) { + body, err := json.Marshal(command) + if err != nil { + panic(err) + } + b.commands = append(b.commands, body) + b.targets = append(b.targets, target) +} + +// Run sends every command in one data.exec call. The batch is all-or-nothing: the first error +// discards every command that already applied. +func (b *Batch) Run() error { + if len(b.commands) == 0 { + return nil + } + result, err := Data{}.Exec(abi.DataExecParameters{Commands: b.commands}) + if err != nil { + return err + } + if len(result.Results) != len(b.targets) { + return errors.New("rudder: data.exec returned a different number of results than commands") + } + for index, raw := range result.Results { + if len(raw) == 0 { + continue + } + if err := json.Unmarshal(raw, b.targets[index]); err != nil { + return err + } + } + b.ran = true + return nil +} + +type Future[Value any] struct { + value Value + batch *Batch +} + +// NewFuture registers a command with the batch. Value panics until Run succeeds: reading a result +// that was never fetched is an author bug, and a trap rolls the call back instead of returning a +// zero score as if it were real. +func NewFuture[Value any](batch *Batch, command any) *Future[Value] { + future := &Future[Value]{batch: batch} + batch.add(command, &future.value) + return future +} + +func (f *Future[Value]) Value() Value { + if !f.batch.ran { + panic("rudder: batch result read before Run") + } + return f.value +} + +// Command registers a raw host command with a batch, so an op with no generated accessor runs in +// the same all-or-nothing batch as the generated ones. It is NOT an escape hatch for ops the +// generator covers. +func Command(batch *Batch, command any) *Future[struct{}] { + return NewFuture[struct{}](batch, command) +} + +var dataHash string + +func SetDataHash(hash string) { dataHash = hash } + +// docEntry is the wire shape of one document, the same one the generated Entry declares. +// DocPatch decodes into it because it is the only reader that does not know the generated type. +type docEntry[Value any] struct { + Key string `json:"key"` + Value Value `json:"value"` + Version Version `json:"version"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// DocPatch is get, apply, put, one retry. It lives here rather than in generated code because it is +// the only author operation with a loop in it, and generated code with a loop needs tests. +// +// Patch retries once on version_conflict, with the same idempotency key: a put that failed does +// not use up its key. +func DocPatch[Value any, Entry any](primitive string, owner Owner, key string, apply func(*Value) error, op []Op) (Entry, error) { + var entry Entry + for attempt := 0; attempt < 2; attempt++ { + current, err := One[docEntry[Value]](abi.DocGetParameters{Primitive: primitive, Owner: owner, Key: key}) + if err != nil { + return entry, err + } + value := current.Value + if err := apply(&value); err != nil { + if errors.Is(err, Unchanged) { + return asEntry[Entry](key, current.Value, current.Version, current.UpdatedAt), nil + } + return entry, err + } + written, err := One[Written](abi.DocPutParameters{ + Primitive: primitive, Owner: owner, Key: key, + Data: JSON(value), Version: int64(current.Version), OpKey: OpKey(op), + }) + switch { + case err == nil: + return asEntry[Entry](key, value, written.Version, time.Time{}), nil + case !IsCode(err, abi.ErrorCodeVersionConflict): + return entry, err + } + } + return entry, Fail(abi.ErrorCodeVersionConflict, "document "+primitive+"/"+key+" changed twice while patching") +} + +// asEntry builds the generated entry type from the four fields every entry has. It goes through +// JSON because DocPatch is compiled once for a type the SDK cannot name; the generated struct is +// the same shape docEntry is, so a failure here is a generator bug and traps the call. +func asEntry[Entry any, Value any](key string, value Value, version Version, updatedAt time.Time) Entry { + var entry Entry + body, err := json.Marshal(docEntry[Value]{Key: key, Value: value, Version: version, UpdatedAt: updatedAt}) + if err == nil { + err = json.Unmarshal(body, &entry) + } + if err != nil { + panic("rudder: generated document entry does not fit the wire shape: " + err.Error()) + } + return entry +} + +// catalogPage fits every catalog result: list and active answer {rows, truncated}, get answers +// {row, found}. One shape is what lets every batched catalog read return *CatalogFuture[Row]. +type catalogPage struct { + Rows []json.RawMessage `json:"rows"` + Row json.RawMessage `json:"row"` + Found bool `json:"found"` +} + +func (p catalogPage) raw() []json.RawMessage { + if p.Found { + return []json.RawMessage{p.Row} + } + return p.Rows +} + +func CatalogAll[Row any](name string) ([]Row, error) { + return catalogRows[Row](abi.CatalogListParameters{Primitive: name, Limit: catalogLimit}) +} + +func CatalogWhere[Row any](name string, field string, equals any) ([]Row, error) { + return catalogRows[Row](abi.CatalogListParameters{ + Primitive: name, Field: field, Eq: catalogEq(equals), Limit: catalogLimit, + }) +} + +func CatalogBy[Row any](name string, field string, equals any) (Row, bool, error) { + return catalogOne[Row](abi.CatalogGetParameters{Primitive: name, Field: field, Eq: catalogEq(equals)}) +} + +// CatalogActive is the rows whose schedule is open now and whose segment this owner matches, in +// the declared order, both evaluated in the kernel. A zero Owner evaluates the schedule only. +func CatalogActive[Row any](name string, owner Owner, field string, equals any) ([]Row, error) { + return catalogRows[Row](catalogActive(name, owner, field, equals, catalogLimit)) +} + +func CatalogActiveOne[Row any](name string, owner Owner, field string, equals any) (Row, bool, error) { + return catalogOne[Row](catalogActive(name, owner, field, equals, 1)) +} + +func catalogActive(name string, owner Owner, field string, equals any, limit int64) abi.CatalogActiveParameters { + return abi.CatalogActiveParameters{ + Primitive: name, Owner: owner, Field: field, Eq: catalogEq(equals), Limit: limit, + } +} + +func catalogRows[Row any](command any) ([]Row, error) { + page, err := One[catalogPage](command) + if err != nil { + return nil, err + } + return decodeRows[Row](page.raw()) +} + +func catalogOne[Row any](command any) (Row, bool, error) { + rows, err := catalogRows[Row](command) + var row Row + if err != nil || len(rows) == 0 { + return row, false, err + } + return rows[0], true, nil +} + +func decodeRows[Row any](items []json.RawMessage) ([]Row, error) { + rows := make([]Row, 0, len(items)) + for _, item := range items { + var row Row + if err := json.Unmarshal(item, &row); err != nil { + return nil, err + } + rows = append(rows, row) + } + return rows, nil +} + +// CatalogFuture is the batched form of a catalog read. The rows decode when the value is read +// rather than in Run, because only the generated package knows the row type; a row that does not +// fit it is a generated-code bug, so it traps the call the way Value does before Run. +type CatalogFuture[Row any] struct { + page *Future[catalogPage] +} + +func newCatalogFuture[Row any](batch *Batch, command any) *CatalogFuture[Row] { + return &CatalogFuture[Row]{page: NewFuture[catalogPage](batch, command)} +} + +func (f *CatalogFuture[Row]) Value() []Row { + rows, err := decodeRows[Row](f.page.Value().raw()) + if err != nil { + panic("rudder: catalog row does not fit the generated type: " + err.Error()) + } + return rows +} + +func (f *CatalogFuture[Row]) One() (Row, bool) { + rows := f.Value() + if len(rows) == 0 { + var row Row + return row, false + } + return rows[0], true +} + +func CatalogAllFuture[Row any](batch *Batch, name string) *CatalogFuture[Row] { + return newCatalogFuture[Row](batch, abi.CatalogListParameters{Primitive: name, Limit: catalogLimit}) +} + +func CatalogWhereFuture[Row any](batch *Batch, name string, field string, equals any) *CatalogFuture[Row] { + return newCatalogFuture[Row](batch, abi.CatalogListParameters{ + Primitive: name, Field: field, Eq: catalogEq(equals), Limit: catalogLimit, + }) +} + +func CatalogByFuture[Row any](batch *Batch, name string, field string, equals any) *CatalogFuture[Row] { + return newCatalogFuture[Row](batch, abi.CatalogGetParameters{Primitive: name, Field: field, Eq: catalogEq(equals)}) +} + +func CatalogActiveFuture[Row any](batch *Batch, name string, owner Owner, field string, equals any) *CatalogFuture[Row] { + return newCatalogFuture[Row](batch, catalogActive(name, owner, field, equals, catalogLimit)) +} + +func CatalogActiveOneFuture[Row any](batch *Batch, name string, owner Owner, field string, equals any) *CatalogFuture[Row] { + return newCatalogFuture[Row](batch, catalogActive(name, owner, field, equals, 1)) +} + +func catalogEq(equals any) json.RawMessage { + if equals == nil { + return nil + } + body, err := json.Marshal(equals) + if err != nil { + panic(err) + } + return body +} + +// Grant pays a reward to an owner. Concurrent grants to the same owner are safe. +func (c *Context) Grant(owner Owner, reward Reward, op ...Op) error { + return call(abi.RewardGrantParameters{ + Owner: owner, + Currencies: amounts(reward.Currencies), + Items: stacks(reward.Items), + OpKey: OpKey(op), + }, nil) +} + +func amounts(values map[string]int64) []abi.Amount { + out := make([]abi.Amount, 0, len(values)) + for _, currency := range sortedKeys(values) { + out = append(out, abi.Amount{Currency: currency, Amount: values[currency]}) + } + return out +} + +func stacks(values map[string]int64) []abi.Stack { + out := make([]abi.Stack, 0, len(values)) + for _, item := range sortedKeys(values) { + out = append(out, abi.Stack{Item: item, Quantity: values[item]}) + } + return out +} + +func sortedKeys(values map[string]int64) []string { + keys := make([]string, 0, len(values)) + for key := range values { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6dd4f4a --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module hub.rudder.build/rudder/rudder-module-go + +go 1.27.0 diff --git a/native.go b/native.go new file mode 100644 index 0000000..861c9fa --- /dev/null +++ b/native.go @@ -0,0 +1,17 @@ +//go:build !wasip1 + +package rudder + +const nativeBuildMessage = "rudder modules run only when built with GOOS=wasip1 GOARCH=wasm" + +func readInput() []byte { + panic(nativeBuildMessage) +} + +func hostCall(request []byte) []byte { + panic(nativeBuildMessage) +} + +func writeOutput(body []byte) { + panic(nativeBuildMessage) +} diff --git a/operations.go b/operations.go new file mode 100644 index 0000000..7c0eee3 --- /dev/null +++ b/operations.go @@ -0,0 +1,115 @@ +package rudder + +import ( + "encoding/json" + + "hub.rudder.build/rudder/rudder-module-go/abi" +) + +type Ledger struct{} + +func (Ledger) Balance(parameters abi.LedgerBalanceParameters) (*abi.LedgerBalanceResult, error) { + return callResult[abi.LedgerBalanceResult](parameters) +} + +func (Ledger) Balances(parameters abi.LedgerBalancesParameters) (*abi.LedgerBalancesResult, error) { + return callResult[abi.LedgerBalancesResult](parameters) +} + +func (Ledger) Credit(parameters abi.LedgerCreditParameters) (*abi.LedgerCreditResult, error) { + return callResult[abi.LedgerCreditResult](parameters) +} + +func (Ledger) Debit(parameters abi.LedgerDebitParameters) (*abi.LedgerDebitResult, error) { + return callResult[abi.LedgerDebitResult](parameters) +} + +func (Ledger) History(parameters abi.LedgerHistoryParameters) (*abi.LedgerHistoryResult, error) { + return callResult[abi.LedgerHistoryResult](parameters) +} + +type Inventory struct{} + +func (Inventory) Get(parameters abi.InventoryGetParameters) (*abi.InventoryGetResult, error) { + return callResult[abi.InventoryGetResult](parameters) +} + +func (Inventory) List(parameters abi.InventoryListParameters) (*abi.InventoryListResult, error) { + return callResult[abi.InventoryListResult](parameters) +} + +func (Inventory) Has(parameters abi.InventoryHasParameters) (*abi.InventoryHasResult, error) { + return callResult[abi.InventoryHasResult](parameters) +} + +func (Inventory) Grant(parameters abi.InventoryGrantParameters) error { + return call(parameters, nil) +} + +func (Inventory) Consume(parameters abi.InventoryConsumeParameters) error { + return call(parameters, nil) +} + +type Storage struct{} + +func (Storage) Get(parameters abi.StorageGetParameters) (*abi.StorageGetResult, error) { + return callResult[abi.StorageGetResult](parameters) +} + +func (Storage) Set(parameters abi.StorageSetParameters) error { + return call(parameters, nil) +} + +type ProjectStorage struct{} + +func (ProjectStorage) Get(parameters abi.ProjectStorageGetParameters) (*abi.ProjectStorageGetResult, error) { + return callResult[abi.ProjectStorageGetResult](parameters) +} + +func (ProjectStorage) Set(parameters abi.ProjectStorageSetParameters) error { + return call(parameters, nil) +} + +type Segments struct{} + +func (Segments) Match(parameters abi.SegmentsMatchParameters) (*abi.SegmentsMatchResult, error) { + return callResult[abi.SegmentsMatchResult](parameters) +} + +func (Segments) Bucket(parameters abi.SegmentsBucketParameters) (*abi.SegmentsBucketResult, error) { + return callResult[abi.SegmentsBucketResult](parameters) +} + +type Events struct{} + +func (Events) Emit(parameters abi.EventsEmitParameters) error { + return call(parameters, nil) +} + +type Data struct{} + +func (Data) Exec(parameters abi.DataExecParameters) (*abi.DataExecResult, error) { + return callResult[abi.DataExecResult](parameters) +} + +type Modules struct{} + +func (Modules) Call(module string, function string, args any) (json.RawMessage, error) { + body, err := json.Marshal(args) + if err != nil { + return nil, err + } + var result abi.ModulesCallResult + if err := call(abi.ModulesCallParameters{Module: module, Function: function, Arguments: body}, &result); err != nil { + return nil, err + } + return result.Value, nil +} + +func callResult[Result any](parameters any) (*Result, error) { + var result Result + if err := call(parameters, &result); err != nil { + return nil, err + } + return &result, nil +} diff --git a/rudder.go b/rudder.go new file mode 100644 index 0000000..84e94c5 --- /dev/null +++ b/rudder.go @@ -0,0 +1,179 @@ +package rudder + +import ( + "encoding/json" + "errors" + "reflect" + "slices" + "sort" + "time" + + "hub.rudder.build/rudder/rudder-module-go/abi" +) + +type Handler struct { + run func(ctx *Context, args json.RawMessage) (any, error) + arguments reflect.Type + result reflect.Type +} + +type Option func(function *function) + +type function struct { + handler Handler + errors []string +} + +type Context struct { + Trigger string + Player *abi.Player + Config json.RawMessage + Now time.Time + Module string + Environment string + + Ledger Ledger + Inventory Inventory + Storage Storage + ProjectStorage ProjectStorage + Segments Segments + Events Events + Modules Modules + Data Data +} + +type Error struct { + Code string + Message string +} + +func (e *Error) Error() string { + return e.Code + ": " + e.Message +} + +func Fail(code string, message string) error { + return &Error{Code: code, Message: message} +} + +var functions = map[string]*function{} + +func Register(name string, handler Handler, options ...Option) { + registered := &function{handler: handler, errors: []string{}} + for _, option := range options { + option(registered) + } + functions[name] = registered +} + +func Errors(codes ...string) Option { + return func(function *function) { + function.errors = append(function.errors, codes...) + } +} + +func Typed[Arguments any, Result any](handler func(ctx *Context, args Arguments) (Result, error)) Handler { + return Handler{ + run: func(ctx *Context, raw json.RawMessage) (any, error) { + var args Arguments + if len(raw) > 0 { + if err := json.Unmarshal(raw, &args); err != nil { + return nil, Fail(abi.ErrorCodeInvalidParameters, err.Error()) + } + } + return handler(ctx, args) + }, + arguments: reflect.TypeFor[Arguments](), + result: reflect.TypeFor[Result](), + } +} + +func runHandle() { + output, err := invoke(readInput()) + if err != nil { + var failure *Error + if !errors.As(err, &failure) { + failure = &Error{Code: abi.ErrorCodeInternal, Message: err.Error()} + } + output = abi.Output{Error: &abi.ErrorBody{Code: failure.Code, Message: failure.Message}} + } + body, err := json.Marshal(output) + if err != nil { + panic(err) + } + writeOutput(body) +} + +func invoke(input []byte) (abi.Output, error) { + var request abi.Input + if err := json.Unmarshal(input, &request); err != nil { + return abi.Output{}, err + } + registered, ok := functions[request.Function] + if !ok { + return abi.Output{}, Fail(abi.ErrorCodeFunctionNotFound, "function "+request.Function+" is not registered") + } + now, err := time.Parse(time.RFC3339Nano, request.Context.Now) + if err != nil { + return abi.Output{}, err + } + ctx := &Context{ + Trigger: request.Context.Trigger, + Player: request.Context.Player, + Config: request.Context.Config, + Now: now, + Module: request.Context.Module, + Environment: request.Context.Environment, + } + result, err := registered.handler.run(ctx, request.Arguments) + if err != nil { + return abi.Output{}, err + } + body, err := json.Marshal(result) + if err != nil { + return abi.Output{}, err + } + return abi.Output{Result: body}, nil +} + +func runDescribe() { + names := make([]string, 0, len(functions)) + for name := range functions { + names = append(names, name) + } + sort.Strings(names) + described := make([]abi.DescribedFunction, 0, len(names)) + for _, name := range names { + registered := functions[name] + errorCodes := slices.Clone(registered.errors) + slices.Sort(errorCodes) + described = append(described, abi.DescribedFunction{ + Name: name, + Input: mustSchema(name+" arguments", registered.handler.arguments), + Output: mustSchema(name+" result", registered.handler.result), + Errors: slices.Compact(errorCodes), + }) + } + body, err := json.Marshal(abi.Describe{Functions: described, DataHash: dataHash}) + if err != nil { + panic(err) + } + writeOutput(body) +} + +func call(parameters any, result any) error { + request, err := json.Marshal(parameters) + if err != nil { + return err + } + var response abi.CallResponse + if err := json.Unmarshal(hostCall(request), &response); err != nil { + return err + } + if response.Error != nil { + return &Error{Code: response.Error.Code, Message: response.Error.Message} + } + if result == nil { + return nil + } + return json.Unmarshal(response.Result, result) +} diff --git a/schema.go b/schema.go new file mode 100644 index 0000000..b4bad12 --- /dev/null +++ b/schema.go @@ -0,0 +1,232 @@ +package rudder + +import ( + "encoding" + "encoding/json" + "fmt" + "reflect" + "regexp" + "slices" + "strconv" + "strings" + "time" +) + +type schema struct { + Type string `json:"type,omitempty"` + Format string `json:"format,omitempty"` + Nullable bool `json:"nullable,omitempty"` + Properties map[string]*schema `json:"properties,omitempty"` + Required []string `json:"required,omitempty"` + Items *schema `json:"items,omitempty"` + AdditionalProperties *schema `json:"additionalProperties,omitempty"` + Enum []any `json:"enum,omitempty"` + Minimum *float64 `json:"minimum,omitempty"` + Maximum *float64 `json:"maximum,omitempty"` + MinLength *uint64 `json:"minLength,omitempty"` + MaxLength *uint64 `json:"maxLength,omitempty"` + Pattern string `json:"pattern,omitempty"` +} + +var ( + timeType = reflect.TypeFor[time.Time]() + jsonMarshalerType = reflect.TypeFor[json.Marshaler]() + textMarshalerType = reflect.TypeFor[encoding.TextMarshaler]() +) + +func mustSchema(label string, t reflect.Type) json.RawMessage { + body, err := json.Marshal(schemaOf(t, map[reflect.Type]bool{})) + if err != nil { + panic(fmt.Sprintf("rudder: %s schema: %v", label, err)) + } + return body +} + +func schemaOf(t reflect.Type, visiting map[reflect.Type]bool) *schema { + if t.Kind() == reflect.Pointer { + element := schemaOf(t.Elem(), visiting) + element.Nullable = true + return element + } + if t == timeType { + return &schema{Type: "string", Format: "date-time"} + } + if t.Implements(jsonMarshalerType) || reflect.PointerTo(t).Implements(jsonMarshalerType) { + return &schema{} + } + if t.Implements(textMarshalerType) || reflect.PointerTo(t).Implements(textMarshalerType) { + return &schema{Type: "string"} + } + switch t.Kind() { + case reflect.Bool: + return &schema{Type: "boolean"} + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return &schema{Type: "integer"} + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + zero := 0.0 + return &schema{Type: "integer", Minimum: &zero} + case reflect.Float32, reflect.Float64: + return &schema{Type: "number"} + case reflect.String: + return &schema{Type: "string"} + case reflect.Interface: + return &schema{} + case reflect.Slice: + if t.Elem().Kind() == reflect.Uint8 { + return &schema{Type: "string", Format: "byte"} + } + return &schema{Type: "array", Items: schemaOf(t.Elem(), visiting)} + case reflect.Array: + return &schema{Type: "array", Items: schemaOf(t.Elem(), visiting)} + case reflect.Map: + if t.Elem().Kind() == reflect.Interface { + return &schema{} + } + return &schema{Type: "object", AdditionalProperties: schemaOf(t.Elem(), visiting)} + case reflect.Struct: + object := &schema{Type: "object", Properties: map[string]*schema{}} + addFields(object, t, visiting) + slices.Sort(object.Required) + return object + } + panic(fmt.Sprintf("rudder: type %s cannot be encoded as JSON", t)) +} + +func addFields(object *schema, t reflect.Type, visiting map[reflect.Type]bool) { + if visiting[t] { + panic(fmt.Sprintf("rudder: recursive type %s is not supported in function arguments and results", t)) + } + visiting[t] = true + defer delete(visiting, t) + var embedded []reflect.Type + for field := range t.Fields() { + tag := field.Tag.Get("json") + if tag == "-" { + continue + } + name, options, _ := strings.Cut(tag, ",") + fieldType := field.Type + if field.Anonymous && name == "" { + if fieldType.Kind() == reflect.Pointer { + fieldType = fieldType.Elem() + } + if fieldType.Kind() == reflect.Struct { + embedded = append(embedded, fieldType) + continue + } + } + if !field.IsExported() { + continue + } + if name == "" { + name = field.Name + } + property := schemaOf(fieldType, visiting) + applyConstraints(property, t.String()+"."+field.Name, field.Tag.Get("rudder")) + object.Properties[name] = property + if !strings.Contains(","+options+",", ",omitempty,") && !strings.Contains(","+options+",", ",omitzero,") { + object.Required = append(object.Required, name) + } + } + for _, embeddedType := range embedded { + inner := &schema{Properties: map[string]*schema{}} + addFields(inner, embeddedType, visiting) + required := map[string]bool{} + for _, name := range inner.Required { + required[name] = true + } + for name, property := range inner.Properties { + if _, ok := object.Properties[name]; ok { + continue + } + object.Properties[name] = property + if required[name] { + object.Required = append(object.Required, name) + } + } + } +} + +func applyConstraints(property *schema, field string, tag string) { + for tag != "" { + var entry string + if strings.HasPrefix(tag, "pattern=") { + entry, tag = tag, "" + } else { + entry, tag, _ = strings.Cut(tag, ",") + } + key, value, _ := strings.Cut(entry, "=") + if err := applyConstraint(property, key, value); err != nil { + panic(fmt.Sprintf("rudder: invalid rudder tag on %s: %s: %v", field, entry, err)) + } + } +} + +func applyConstraint(property *schema, key string, value string) error { + numeric := property.Type == "integer" || property.Type == "number" + switch key { + case "min", "max": + if !numeric { + return fmt.Errorf("%s needs an integer or number field, got %q", key, property.Type) + } + bound, err := strconv.ParseFloat(value, 64) + if err != nil { + return err + } + if key == "min" { + property.Minimum = &bound + } else { + property.Maximum = &bound + } + case "minLength", "maxLength": + if property.Type != "string" { + return fmt.Errorf("%s needs a string field, got %q", key, property.Type) + } + length, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return err + } + if key == "minLength" { + property.MinLength = &length + } else { + property.MaxLength = &length + } + case "pattern": + if property.Type != "string" { + return fmt.Errorf("pattern needs a string field, got %q", property.Type) + } + if _, err := regexp.Compile(value); err != nil { + return err + } + property.Pattern = value + case "format": + if property.Type == "" || value == "" { + return fmt.Errorf("format needs a typed field and a value") + } + property.Format = value + case "enum": + for option := range strings.SplitSeq(value, "|") { + switch property.Type { + case "string": + property.Enum = append(property.Enum, option) + case "integer": + parsed, err := strconv.ParseInt(option, 10, 64) + if err != nil { + return err + } + property.Enum = append(property.Enum, parsed) + case "number": + parsed, err := strconv.ParseFloat(option, 64) + if err != nil { + return err + } + property.Enum = append(property.Enum, parsed) + default: + return fmt.Errorf("enum needs a string, integer or number field, got %q", property.Type) + } + } + default: + return fmt.Errorf("unknown constraint %q; use min, max, minLength, maxLength, pattern, enum or format", key) + } + return nil +} diff --git a/wasm.go b/wasm.go new file mode 100644 index 0000000..4c3824a --- /dev/null +++ b/wasm.go @@ -0,0 +1,46 @@ +//go:build wasip1 + +package rudder + +import "unsafe" + +//go:wasmimport rudder_v1 input_len +func importInputLength() uint32 + +//go:wasmimport rudder_v1 input +func importInput(pointer unsafe.Pointer) + +//go:wasmimport rudder_v1 call +func importCall(pointer unsafe.Pointer, length uint32) uint32 + +//go:wasmimport rudder_v1 result +func importResult(pointer unsafe.Pointer) + +//go:wasmimport rudder_v1 output +func importOutput(pointer unsafe.Pointer, length uint32) + +//go:wasmexport handle +func handle() { + runHandle() +} + +//go:wasmexport describe +func describe() { + runDescribe() +} + +func readInput() []byte { + input := make([]byte, importInputLength()) + importInput(unsafe.Pointer(unsafe.SliceData(input))) + return input +} + +func hostCall(request []byte) []byte { + response := make([]byte, importCall(unsafe.Pointer(unsafe.SliceData(request)), uint32(len(request)))) + importResult(unsafe.Pointer(unsafe.SliceData(response))) + return response +} + +func writeOutput(body []byte) { + importOutput(unsafe.Pointer(unsafe.SliceData(body)), uint32(len(body))) +}