5 Commits

Author SHA1 Message Date
Pijus Kamandulis
f5b8453995 Support patch operations 'set' and 'incr' #7 2024-12-25 23:32:50 +02:00
Pijus Kamandulis
928ca29fe4 Support parameter in bracket #8 2024-12-25 21:28:42 +02:00
Pijus Kamandulis
39cd9e2357 Update dependancies 2024-12-20 20:27:42 +02:00
Pijus Kamandulis
bcf4b513b6 Expose repository functions to sharedlibs 2024-12-20 20:25:32 +02:00
Pijus Kamandulis
363f822e5a Added some tests for sharedlibrary 2024-12-19 23:21:45 +02:00
25 changed files with 2163 additions and 1364 deletions

View File

@@ -13,6 +13,12 @@ GOVERSION=1.22.0
DIST_DIR=dist
SHARED_LIB_TEST_CC=gcc
SHARED_LIB_TEST_CFLAGS=-Wall -ldl
SHARED_LIB_TEST_TARGET=$(DIST_DIR)/sharedlibrary_test
SHARED_LIB_TEST_DIR=./sharedlibrary/tests
SHARED_LIB_TEST_SOURCES=$(wildcard $(SHARED_LIB_TEST_DIR)/*.c)
all: test build-all
build-all: build-darwin-arm64 build-darwin-amd64 build-linux-amd64 build-linux-arm64 build-windows-amd64 build-windows-arm64
@@ -45,6 +51,14 @@ build-sharedlib-linux-amd64:
@echo "Building shared library for Linux x64..."
@GOOS=linux GOARCH=amd64 $(GOBUILD) $(SHARED_LIB_OPT) -o $(DIST_DIR)/$(BINARY_NAME)-linux-amd64.so $(SHARED_LIB_LOCATION)
build-sharedlib-tests: build-sharedlib-linux-amd64
@echo "Building shared library tests..."
@$(SHARED_LIB_TEST_CC) $(SHARED_LIB_TEST_CFLAGS) -o $(SHARED_LIB_TEST_TARGET) $(SHARED_LIB_TEST_SOURCES)
run-sharedlib-tests: build-sharedlib-tests
@echo "Running shared library tests..."
@$(SHARED_LIB_TEST_TARGET) $(DIST_DIR)/$(BINARY_NAME)-linux-amd64.so
xgo-compile-sharedlib:
@echo "Building shared libraries using xgo..."
@mkdir -p $(DIST_DIR)

View File

@@ -57,6 +57,18 @@ func (c *ServerConfig) PopulateCalculatedFields() {
logger.EnableDebugOutput = c.Debug
}
func (c *ServerConfig) ApplyDefaultsToEmptyFields() {
if c.Host == "" {
c.Host = "localhost"
}
if c.Port == 0 {
c.Port = 8081
}
if c.AccountKey == "" {
c.AccountKey = DefaultAccountKey
}
}
func setFlagsFromEnvironment() (err error) {
flag.VisitAll(func(f *flag.Flag) {
name := EnvPrefix + strings.ToUpper(strings.Replace(f.Name, "-", "_", -1))

View File

@@ -1,20 +1,20 @@
package config
type ServerConfig struct {
DatabaseAccount string
DatabaseDomain string
DatabaseEndpoint string
AccountKey string
DatabaseAccount string `json:"databaseAccount"`
DatabaseDomain string `json:"databaseDomain"`
DatabaseEndpoint string `json:"databaseEndpoint"`
AccountKey string `json:"accountKey"`
ExplorerPath string
Port int
Host string
TLS_CertificatePath string
TLS_CertificateKey string
InitialDataFilePath string
PersistDataFilePath string
DisableAuth bool
DisableTls bool
Debug bool
ExplorerBaseUrlLocation string
ExplorerPath string `json:"explorerPath"`
Port int `json:"port"`
Host string `json:"host"`
TLS_CertificatePath string `json:"tlsCertificatePath"`
TLS_CertificateKey string `json:"tlsCertificateKey"`
InitialDataFilePath string `json:"initialDataFilePath"`
PersistDataFilePath string `json:"persistDataFilePath"`
DisableAuth bool `json:"disableAuth"`
DisableTls bool `json:"disableTls"`
Debug bool `json:"debug"`
ExplorerBaseUrlLocation string `json:"explorerBaseUrlLocation"`
}

View File

@@ -6,11 +6,11 @@ import (
"net/http"
"strconv"
jsonpatch "github.com/evanphx/json-patch/v5"
"github.com/gin-gonic/gin"
"github.com/pikami/cosmium/internal/constants"
"github.com/pikami/cosmium/internal/logger"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
jsonpatch "github.com/pikami/json-patch/v5"
)
func (h *Handlers) GetAllDocuments(c *gin.Context) {

View File

@@ -147,6 +147,21 @@ func Test_Documents(t *testing.T) {
)
})
t.Run("Should query document with query parameters as accessor", func(t *testing.T) {
testCosmosQuery(t, collectionClient,
`select c.id
FROM c
WHERE c[@param]="67890"
ORDER BY c.id`,
[]azcosmos.QueryParameter{
{Name: "@param", Value: "id"},
},
[]interface{}{
map[string]interface{}{"id": "67890"},
},
)
})
t.Run("Should query array accessor", func(t *testing.T) {
testCosmosQuery(t, collectionClient,
`SELECT c.id,
@@ -220,11 +235,14 @@ func Test_Documents_Patch(t *testing.T) {
t.Run("Should PATCH document", func(t *testing.T) {
context := context.TODO()
expectedData := map[string]interface{}{"id": "67890", "pk": "456", "newField": "newValue"}
expectedData := map[string]interface{}{"id": "67890", "pk": "666", "newField": "newValue", "incr": 15., "setted": "isSet"}
patch := azcosmos.PatchOperations{}
patch.AppendAdd("/newField", "newValue")
patch.AppendIncrement("/incr", 15)
patch.AppendRemove("/isCool")
patch.AppendReplace("/pk", "666")
patch.AppendSet("/setted", "isSet")
itemResponse, err := collectionClient.PatchItem(
context,
@@ -237,13 +255,15 @@ func Test_Documents_Patch(t *testing.T) {
)
assert.Nil(t, err)
var itemResponseBody map[string]string
var itemResponseBody map[string]interface{}
json.Unmarshal(itemResponse.Value, &itemResponseBody)
assert.Equal(t, expectedData["id"], itemResponseBody["id"])
assert.Equal(t, expectedData["pk"], itemResponseBody["pk"])
assert.Empty(t, itemResponseBody["isCool"])
assert.Equal(t, expectedData["newField"], itemResponseBody["newField"])
assert.Equal(t, expectedData["incr"], itemResponseBody["incr"])
assert.Equal(t, expectedData["setted"], itemResponseBody["setted"])
})
t.Run("Should not allow to PATCH document ID", func(t *testing.T) {

4
go.mod
View File

@@ -5,9 +5,9 @@ go 1.22.0
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2
github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v0.3.6
github.com/evanphx/json-patch/v5 v5.9.0
github.com/gin-gonic/gin v1.10.0
github.com/google/uuid v1.6.0
github.com/pikami/json-patch/v5 v5.9.2
github.com/stretchr/testify v1.9.0
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67
)
@@ -39,7 +39,7 @@ require (
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.12.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/net v0.32.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.36.0 // indirect

8
go.sum
View File

@@ -22,8 +22,6 @@ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQ
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
@@ -66,6 +64,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pikami/json-patch/v5 v5.9.2 h1:ciTlocWccYVE3DEa45dsMm02c/tOvcaBY7PpEUNZhrU=
github.com/pikami/json-patch/v5 v5.9.2/go.mod h1:eJIScZ4xgf2aBHLi2UMzYtjlWESUBDOBf7EAx3JW0nI=
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI=
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -92,8 +92,8 @@ golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo=
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

View File

@@ -39,20 +39,33 @@ func (r *DataRepository) LoadStateFS(filePath string) {
return
}
var state repositorymodels.State
if err := json.Unmarshal(data, &state); err != nil {
err = r.LoadStateJSON(string(data))
if err != nil {
log.Fatalf("Error unmarshalling state JSON: %v", err)
return
}
}
logger.Info("Loaded state:")
logger.Infof("Databases: %d\n", getLength(state.Databases))
logger.Infof("Collections: %d\n", getLength(state.Collections))
logger.Infof("Documents: %d\n", getLength(state.Documents))
func (r *DataRepository) LoadStateJSON(jsonData string) error {
r.storeState.RLock()
defer r.storeState.RUnlock()
r.storeState = state
var state repositorymodels.State
if err := json.Unmarshal([]byte(jsonData), &state); err != nil {
return err
}
r.storeState.Collections = state.Collections
r.storeState.Databases = state.Databases
r.storeState.Documents = state.Documents
r.ensureStoreStateNoNullReferences()
logger.Info("Loaded state:")
logger.Infof("Databases: %d\n", getLength(r.storeState.Databases))
logger.Infof("Collections: %d\n", getLength(r.storeState.Collections))
logger.Infof("Documents: %d\n", getLength(r.storeState.Documents))
return nil
}
func (r *DataRepository) SaveStateFS(filePath string) {

File diff suppressed because it is too large Load Diff

View File

@@ -325,6 +325,7 @@ DotFieldAccess <- "." id:Identifier {
ArrayFieldAccess <- "[\"" id:Identifier "\"]" { return id, nil }
/ "[" id:Integer "]" { return strconv.Itoa(id.(int)), nil }
/ "[" id:ParameterConstant "]" { return id.(parsers.Constant).Value.(string), nil }
Identifier <- [a-zA-Z_][a-zA-Z0-9_]* {
return string(c.text), nil

View File

@@ -22,6 +22,20 @@ func Test_Parse_Select(t *testing.T) {
)
})
t.Run("Should parse SELECT with query parameters as accessor", func(t *testing.T) {
testQueryParse(
t,
`SELECT c.id, c[@param] FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{Path: []string{"c", "id"}},
{Path: []string{"c", "@param"}},
},
Table: parsers.Table{Value: "c"},
},
)
})
t.Run("Should parse SELECT DISTINCT", func(t *testing.T) {
testQueryParse(
t,

View File

@@ -317,6 +317,10 @@ func (r rowContext) applyProjection(selectItems []parsers.SelectItem) RowType {
} else {
destinationName = fmt.Sprintf("$%d", index+1)
}
if destinationName[0] == '@' {
destinationName = r.parameters[destinationName].(string)
}
}
row[destinationName] = r.resolveSelectItem(selectItem)
@@ -572,6 +576,9 @@ func (r rowContext) selectItem_SelectItemTypeField(selectItem parsers.SelectItem
if len(selectItem.Path) > 1 {
for _, pathSegment := range selectItem.Path[1:] {
if pathSegment[0] == '@' {
pathSegment = r.parameters[pathSegment].(string)
}
switch nestedValue := value.(type) {
case map[string]interface{}:

View File

@@ -35,6 +35,29 @@ func Test_Execute_Select(t *testing.T) {
)
})
t.Run("Should execute SELECT with query parameters as accessor", func(t *testing.T) {
testQueryExecute(
t,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{Path: []string{"c", "id"}},
{Path: []string{"c", "@param"}},
},
Table: parsers.Table{Value: "c"},
Parameters: map[string]interface{}{
"@param": "pk",
},
},
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"id": "12345", "pk": 123},
map[string]interface{}{"id": "67890", "pk": 456},
map[string]interface{}{"id": "456", "pk": 456},
map[string]interface{}{"id": "123", "pk": 456},
},
)
})
t.Run("Should execute SELECT DISTINCT", func(t *testing.T) {
testQueryExecute(
t,

View File

@@ -0,0 +1,89 @@
package main
import "C"
import (
"encoding/json"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
)
//export CreateCollection
func CreateCollection(serverName *C.char, databaseId *C.char, collectionJson *C.char) int {
serverNameStr := C.GoString(serverName)
databaseIdStr := C.GoString(databaseId)
collectionStr := C.GoString(collectionJson)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return ResponseServerInstanceNotFound
}
var collection repositorymodels.Collection
err := json.Unmarshal([]byte(collectionStr), &collection)
if err != nil {
return ResponseFailedToParseRequest
}
_, code := serverInstance.repository.CreateCollection(databaseIdStr, collection)
return repositoryStatusToResponseCode(code)
}
//export GetCollection
func GetCollection(serverName *C.char, databaseId *C.char, collectionId *C.char) *C.char {
serverNameStr := C.GoString(serverName)
databaseIdStr := C.GoString(databaseId)
collectionIdStr := C.GoString(collectionId)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return C.CString("")
}
collection, code := serverInstance.repository.GetCollection(databaseIdStr, collectionIdStr)
if code != repositorymodels.StatusOk {
return C.CString("")
}
collectionJson, _ := json.Marshal(collection)
return C.CString(string(collectionJson))
}
//export GetAllCollections
func GetAllCollections(serverName *C.char, databaseId *C.char) *C.char {
serverNameStr := C.GoString(serverName)
databaseIdStr := C.GoString(databaseId)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return C.CString("")
}
collections, code := serverInstance.repository.GetAllCollections(databaseIdStr)
if code != repositorymodels.StatusOk {
return C.CString("")
}
collectionsJson, _ := json.Marshal(collections)
return C.CString(string(collectionsJson))
}
//export DeleteCollection
func DeleteCollection(serverName *C.char, databaseId *C.char, collectionId *C.char) int {
serverNameStr := C.GoString(serverName)
databaseIdStr := C.GoString(databaseId)
collectionIdStr := C.GoString(collectionId)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return ResponseServerInstanceNotFound
}
code := serverInstance.repository.DeleteCollection(databaseIdStr, collectionIdStr)
return repositoryStatusToResponseCode(code)
}

View File

@@ -0,0 +1,89 @@
package main
import "C"
import (
"encoding/json"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
)
//export CreateDatabase
func CreateDatabase(serverName *C.char, databaseJson *C.char) int {
serverNameStr := C.GoString(serverName)
databaseStr := C.GoString(databaseJson)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return ResponseServerInstanceNotFound
}
var database repositorymodels.Database
err := json.Unmarshal([]byte(databaseStr), &database)
if err != nil {
return ResponseFailedToParseRequest
}
_, code := serverInstance.repository.CreateDatabase(database)
return repositoryStatusToResponseCode(code)
}
//export GetDatabase
func GetDatabase(serverName *C.char, databaseId *C.char) *C.char {
serverNameStr := C.GoString(serverName)
databaseIdStr := C.GoString(databaseId)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return C.CString("")
}
database, code := serverInstance.repository.GetDatabase(databaseIdStr)
if code != repositorymodels.StatusOk {
return C.CString("")
}
databaseJson, _ := json.Marshal(database)
return C.CString(string(databaseJson))
}
//export GetAllDatabases
func GetAllDatabases(serverName *C.char) *C.char {
serverNameStr := C.GoString(serverName)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return C.CString("")
}
databases, code := serverInstance.repository.GetAllDatabases()
if code != repositorymodels.StatusOk {
return C.CString("")
}
databasesJson, err := json.Marshal(databases)
if err != nil {
return C.CString("")
}
return C.CString(string(databasesJson))
}
//export DeleteDatabase
func DeleteDatabase(serverName *C.char, databaseId *C.char) int {
serverNameStr := C.GoString(serverName)
databaseIdStr := C.GoString(databaseId)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return ResponseServerInstanceNotFound
}
code := serverInstance.repository.DeleteDatabase(databaseIdStr)
return repositoryStatusToResponseCode(code)
}

122
sharedlibrary/documents.go Normal file
View File

@@ -0,0 +1,122 @@
package main
import "C"
import (
"encoding/json"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
)
//export CreateDocument
func CreateDocument(serverName *C.char, databaseId *C.char, collectionId *C.char, documentJson *C.char) int {
serverNameStr := C.GoString(serverName)
databaseIdStr := C.GoString(databaseId)
collectionIdStr := C.GoString(collectionId)
documentStr := C.GoString(documentJson)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return ResponseServerInstanceNotFound
}
var document repositorymodels.Document
err := json.Unmarshal([]byte(documentStr), &document)
if err != nil {
return ResponseFailedToParseRequest
}
_, code := serverInstance.repository.CreateDocument(databaseIdStr, collectionIdStr, document)
return repositoryStatusToResponseCode(code)
}
//export GetDocument
func GetDocument(serverName *C.char, databaseId *C.char, collectionId *C.char, documentId *C.char) *C.char {
serverNameStr := C.GoString(serverName)
databaseIdStr := C.GoString(databaseId)
collectionIdStr := C.GoString(collectionId)
documentIdStr := C.GoString(documentId)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return C.CString("")
}
document, code := serverInstance.repository.GetDocument(databaseIdStr, collectionIdStr, documentIdStr)
if code != repositorymodels.StatusOk {
return C.CString("")
}
documentJson, _ := json.Marshal(document)
return C.CString(string(documentJson))
}
//export GetAllDocuments
func GetAllDocuments(serverName *C.char, databaseId *C.char, collectionId *C.char) *C.char {
serverNameStr := C.GoString(serverName)
databaseIdStr := C.GoString(databaseId)
collectionIdStr := C.GoString(collectionId)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return C.CString("")
}
documents, code := serverInstance.repository.GetAllDocuments(databaseIdStr, collectionIdStr)
if code != repositorymodels.StatusOk {
return C.CString("")
}
documentsJson, _ := json.Marshal(documents)
return C.CString(string(documentsJson))
}
//export UpdateDocument
func UpdateDocument(serverName *C.char, databaseId *C.char, collectionId *C.char, documentId *C.char, documentJson *C.char) int {
serverNameStr := C.GoString(serverName)
databaseIdStr := C.GoString(databaseId)
collectionIdStr := C.GoString(collectionId)
documentIdStr := C.GoString(documentId)
documentStr := C.GoString(documentJson)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return ResponseServerInstanceNotFound
}
var document repositorymodels.Document
err := json.Unmarshal([]byte(documentStr), &document)
if err != nil {
return ResponseFailedToParseRequest
}
code := serverInstance.repository.DeleteDocument(databaseIdStr, collectionIdStr, documentIdStr)
if code != repositorymodels.StatusOk {
return repositoryStatusToResponseCode(code)
}
_, code = serverInstance.repository.CreateDocument(databaseIdStr, collectionIdStr, document)
return repositoryStatusToResponseCode(code)
}
//export DeleteDocument
func DeleteDocument(serverName *C.char, databaseId *C.char, collectionId *C.char, documentId *C.char) int {
serverNameStr := C.GoString(serverName)
databaseIdStr := C.GoString(databaseId)
collectionIdStr := C.GoString(collectionId)
documentIdStr := C.GoString(documentId)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return ResponseServerInstanceNotFound
}
code := serverInstance.repository.DeleteDocument(databaseIdStr, collectionIdStr, documentIdStr)
return repositoryStatusToResponseCode(code)
}

86
sharedlibrary/shared.go Normal file
View File

@@ -0,0 +1,86 @@
package main
import (
"sync"
"github.com/pikami/cosmium/api"
"github.com/pikami/cosmium/internal/repositories"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
)
type ServerInstance struct {
server *api.ApiServer
repository *repositories.DataRepository
}
var serverInstances map[string]*ServerInstance
var mutex sync.RWMutex
const (
ResponseSuccess = 0
ResponseUnknown = 100
ResponseFailedToParseConfiguration = 101
ResponseFailedToLoadState = 102
ResponseFailedToParseRequest = 103
ResponseServerInstanceAlreadyExists = 104
ResponseServerInstanceNotFound = 105
ResponseRepositoryNotFound = 200
ResponseRepositoryConflict = 201
ResponseRepositoryBadRequest = 202
)
func getInstance(serverName string) (*ServerInstance, bool) {
mutex.RLock()
defer mutex.RUnlock()
if serverInstances == nil {
serverInstances = make(map[string]*ServerInstance)
}
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = serverInstances[serverName]; !ok {
return nil, false
}
return serverInstance, true
}
func addInstance(serverName string, serverInstance *ServerInstance) {
mutex.Lock()
defer mutex.Unlock()
if serverInstances == nil {
serverInstances = make(map[string]*ServerInstance)
}
serverInstances[serverName] = serverInstance
}
func removeInstance(serverName string) {
mutex.Lock()
defer mutex.Unlock()
if serverInstances == nil {
return
}
delete(serverInstances, serverName)
}
func repositoryStatusToResponseCode(status repositorymodels.RepositoryStatus) int {
switch status {
case repositorymodels.StatusOk:
return ResponseSuccess
case repositorymodels.StatusNotFound:
return ResponseRepositoryNotFound
case repositorymodels.Conflict:
return ResponseRepositoryConflict
case repositorymodels.BadRequest:
return ResponseRepositoryBadRequest
default:
return ResponseUnknown
}
}

View File

@@ -9,31 +9,12 @@ import (
"github.com/pikami/cosmium/internal/repositories"
)
type ServerInstance struct {
server *api.ApiServer
repository *repositories.DataRepository
}
var serverInstances map[string]*ServerInstance
const (
ResponseSuccess = 0
ResponseUnknown = 1
ResponseServerInstanceAlreadyExists = 2
ResponseFailedToParseConfiguration = 3
ResponseServerInstanceNotFound = 4
)
//export CreateServerInstance
func CreateServerInstance(serverName *C.char, configurationJSON *C.char) int {
configStr := C.GoString(configurationJSON)
serverNameStr := C.GoString(serverName)
if serverInstances == nil {
serverInstances = make(map[string]*ServerInstance)
}
if _, ok := serverInstances[serverNameStr]; ok {
if _, ok := getInstance(serverNameStr); ok {
return ResponseServerInstanceAlreadyExists
}
@@ -44,6 +25,7 @@ func CreateServerInstance(serverName *C.char, configurationJSON *C.char) int {
}
configuration.PopulateCalculatedFields()
configuration.ApplyDefaultsToEmptyFields()
repository := repositories.NewDataRepository(repositories.RepositoryOptions{
InitialDataFilePath: configuration.InitialDataFilePath,
@@ -53,19 +35,21 @@ func CreateServerInstance(serverName *C.char, configurationJSON *C.char) int {
server := api.NewApiServer(repository, configuration)
server.Start()
serverInstances[serverNameStr] = &ServerInstance{
addInstance(serverNameStr, &ServerInstance{
server: server,
repository: repository,
}
})
return ResponseSuccess
}
//export StopServerInstance
func StopServerInstance(serverName *C.char) int {
if serverInstance, ok := serverInstances[C.GoString(serverName)]; ok {
serverNameStr := C.GoString(serverName)
if serverInstance, ok := getInstance(serverNameStr); ok {
serverInstance.server.Stop()
delete(serverInstances, C.GoString(serverName))
removeInstance(serverNameStr)
return ResponseSuccess
}
@@ -74,7 +58,9 @@ func StopServerInstance(serverName *C.char) int {
//export GetServerInstanceState
func GetServerInstanceState(serverName *C.char) *C.char {
if serverInstance, ok := serverInstances[C.GoString(serverName)]; ok {
serverNameStr := C.GoString(serverName)
if serverInstance, ok := getInstance(serverNameStr); ok {
stateJSON, err := serverInstance.repository.GetState()
if err != nil {
return nil
@@ -85,4 +71,20 @@ func GetServerInstanceState(serverName *C.char) *C.char {
return nil
}
//export LoadServerInstanceState
func LoadServerInstanceState(serverName *C.char, stateJSON *C.char) int {
serverNameStr := C.GoString(serverName)
stateJSONStr := C.GoString(stateJSON)
if serverInstance, ok := getInstance(serverNameStr); ok {
err := serverInstance.repository.LoadStateJSON(stateJSONStr)
if err != nil {
return ResponseFailedToLoadState
}
return ResponseSuccess
}
return ResponseServerInstanceNotFound
}
func main() {}

View File

@@ -0,0 +1,46 @@
#include "shared.h"
int test_CreateServerInstance();
int test_StopServerInstance();
int test_ServerInstanceStateMethods();
int test_Databases();
int main(int argc, char *argv[])
{
if (argc < 2)
{
fprintf(stderr, "Usage: %s <path_to_shared_library>\n", argv[0]);
return EXIT_FAILURE;
}
const char *libPath = argv[1];
handle = dlopen(libPath, RTLD_LAZY);
if (!handle)
{
fprintf(stderr, "Failed to load shared library: %s\n", dlerror());
return EXIT_FAILURE;
}
printf("Running tests for library: %s\n", libPath);
int results[] = {
test_CreateServerInstance(),
test_Databases(),
test_ServerInstanceStateMethods(),
test_StopServerInstance(),
};
int numTests = sizeof(results) / sizeof(results[0]);
int numPassed = 0;
for (int i = 0; i < numTests; i++)
{
if (results[i])
{
numPassed++;
}
}
printf("Tests passed: %d/%d\n", numPassed, numTests);
dlclose(handle);
return EXIT_SUCCESS;
}

View File

@@ -0,0 +1,36 @@
#include "shared.h"
void *handle = NULL;
void *load_function(const char *func_name)
{
void *func = dlsym(handle, func_name);
if (!func)
{
fprintf(stderr, "Failed to load function %s: %s\n", func_name, dlerror());
}
return func;
}
char *compact_json(const char *json)
{
size_t len = strlen(json);
char *compact = (char *)malloc(len + 1);
if (!compact)
{
fprintf(stderr, "Failed to allocate memory for compacted JSON\n");
return NULL;
}
char *dest = compact;
for (const char *src = json; *src != '\0'; ++src)
{
if (!isspace((unsigned char)*src)) // Skip spaces, newlines, tabs, etc.
{
*dest++ = *src;
}
}
*dest = '\0'; // Null-terminate the string
return compact;
}

View File

@@ -0,0 +1,15 @@
#ifndef SHARED_H
#define SHARED_H
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <string.h>
#include <ctype.h>
extern void *handle;
void *load_function(const char *func_name);
char *compact_json(const char *json);
#endif

View File

@@ -0,0 +1,29 @@
#include "shared.h"
int test_CreateServerInstance()
{
typedef int (*CreateServerInstanceFn)(char *, char *);
CreateServerInstanceFn CreateServerInstance = (CreateServerInstanceFn)load_function("CreateServerInstance");
if (!CreateServerInstance)
{
fprintf(stderr, "Failed to find CreateServerInstance function\n");
return 0;
}
char *serverName = "TestServer";
char *configJSON = "{\"host\":\"localhost\",\"port\":8080}";
int result = CreateServerInstance(serverName, configJSON);
if (result == 0)
{
printf("CreateServerInstance: SUCCESS\n");
}
else
{
printf("CreateServerInstance: FAILED (result = %d)\n", result);
return 0;
}
return 1;
}

View File

@@ -0,0 +1,47 @@
#include "shared.h"
int test_Databases()
{
typedef int (*CreateDatabaseFn)(char *, char *);
CreateDatabaseFn CreateDatabase = (CreateDatabaseFn)load_function("CreateDatabase");
if (!CreateDatabase)
{
fprintf(stderr, "Failed to find CreateDatabase function\n");
return 0;
}
char *serverName = "TestServer";
char *configJSON = "{\"id\":\"test-db\"}";
int result = CreateDatabase(serverName, configJSON);
if (result == 0)
{
printf("CreateDatabase: SUCCESS\n");
}
else
{
printf("CreateDatabase: FAILED (result = %d)\n", result);
return 0;
}
typedef char *(*GetDatabaseFn)(char *, char *);
GetDatabaseFn GetDatabase = (GetDatabaseFn)load_function("GetDatabase");
if (!GetDatabase)
{
fprintf(stderr, "Failed to find GetDatabase function\n");
return 0;
}
char *database = GetDatabase(serverName, "test-db");
if (database)
{
printf("GetDatabase: SUCCESS (database = %s)\n", database);
}
else
{
printf("GetDatabase: FAILED\n");
return 0;
}
return 1;
}

View File

@@ -0,0 +1,68 @@
#include "shared.h"
int test_ServerInstanceStateMethods()
{
typedef int (*LoadServerInstanceStateFn)(char *, char *);
LoadServerInstanceStateFn LoadServerInstanceState = (LoadServerInstanceStateFn)load_function("LoadServerInstanceState");
if (!LoadServerInstanceState)
{
fprintf(stderr, "Failed to find LoadServerInstanceState function\n");
return 0;
}
char *serverName = "TestServer";
char *stateJSON = "{\"databases\":{\"test-db\":{\"id\":\"test-db\"}}}";
int result = LoadServerInstanceState(serverName, stateJSON);
if (result == 0)
{
printf("LoadServerInstanceState: SUCCESS\n");
}
else
{
printf("LoadServerInstanceState: FAILED (result = %d)\n", result);
return 0;
}
typedef char *(*GetServerInstanceStateFn)(char *);
GetServerInstanceStateFn GetServerInstanceState = (GetServerInstanceStateFn)load_function("GetServerInstanceState");
if (!GetServerInstanceState)
{
fprintf(stderr, "Failed to find GetServerInstanceState function\n");
return 0;
}
char *state = GetServerInstanceState(serverName);
if (state)
{
printf("GetServerInstanceState: SUCCESS (state = %s)\n", state);
}
else
{
printf("GetServerInstanceState: FAILED\n");
return 0;
}
const char *expected_state = "{\"databases\":{\"test-db\":{\"id\":\"test-db\",\"_ts\":0,\"_rid\":\"\",\"_etag\":\"\",\"_self\":\"\"}},\"collections\":{\"test-db\":{}},\"documents\":{\"test-db\":{}}}";
char *compact_state = compact_json(state);
if (!compact_state)
{
free(state);
return 0;
}
if (strcmp(compact_state, expected_state) == 0)
{
printf("GetServerInstanceState: State matches expected value.\n");
}
else
{
printf("GetServerInstanceState: State does not match expected value.\n");
printf("Expected: %s\n", expected_state);
printf("Actual: %s\n", compact_state);
return 0;
}
free(state);
free(compact_state);
return 1;
}

View File

@@ -0,0 +1,27 @@
#include "shared.h"
int test_StopServerInstance()
{
typedef int (*StopServerInstanceFn)(char *);
StopServerInstanceFn StopServerInstance = (StopServerInstanceFn)load_function("StopServerInstance");
if (!StopServerInstance)
{
fprintf(stderr, "Failed to find StopServerInstance function\n");
return 0;
}
char *serverName = "TestServer";
int result = StopServerInstance(serverName);
if (result == 0)
{
printf("StopServerInstance: SUCCESS\n");
}
else
{
printf("StopServerInstance: FAILED (result = %d)\n", result);
return 0;
}
return 1;
}