diff --git a/.gitignore b/.gitignore index c597668..1630da0 100644 --- a/.gitignore +++ b/.gitignore @@ -171,4 +171,6 @@ $RECYCLE.BIN/ # End of https://www.gitignore.io/api/go,osx,linux,windows,jetbrains+all -config.json \ No newline at end of file +config.json +/core +data.json \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..94c6a28 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018 StratoAPI Team + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..aaa93a4 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +GOCMD=go +GOBUILD=$(GOCMD) build +GOMOD=$(GOCMD) mod +BINARY_NAME=core + +all: build + +build: + $(GOBUILD) -o $(BINARY_NAME) cmd/cli/main.go + +tidy: + $(GOMOD) tidy + +run: + $(MAKE) build + ./$(BINARY_NAME) \ No newline at end of file diff --git a/README.md b/README.md index 33fb9dd..b45be7f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ -# ResourceAPI +# StratoAPI Core A generic data storage API with multiple entrypoints (REST, GraphQL, WS, GRPC, etc), and supports multiple backends (Flatfile, SQLite, SQL, NoSQL, etc). + +### Competitors + +There are various competitors in this field, where each of them provide various benefits. Here are ours: + +* Completely FOSS without any locked features +* Self-hosted, which means compliance with local data storage regulations +* Modular with support for various storage types and usage scenarios +* Stateless design for scalability \ No newline at end of file diff --git a/api.go b/api.go index f4d6ef3..407f52b 100644 --- a/api.go +++ b/api.go @@ -1,33 +1,52 @@ package Core import ( - "github.com/ResourceAPI/Core/config" - "github.com/ResourceAPI/Core/plugins" - "github.com/ResourceAPI/Core/schema" + "github.com/StratoAPI/Core/config" + "github.com/StratoAPI/Core/filter" + "github.com/StratoAPI/Core/middleware" + "github.com/StratoAPI/Core/registry" + "github.com/StratoAPI/Core/resource" + "github.com/StratoAPI/Core/schema" ) func Run() { // Initialize Core config.InitializeConfig() + registry.InitializeRegistry() schema.InitializeSchemas() + resource.InitializeResources() + filter.InitializeFilters() + middleware.InitializeMiddleware() // Initialize Plugins - plugins.InitializePlugins() - plugins.InitializeStores() - plugins.InitializeFilters() - plugins.InitializeFacades() + registry.InitializePlugins(config.Get().PluginDirectory) + + // Initialize Plugin Configs + config.InitializePluginConfigs() + + // Initialize Components + registry.InitializeStores() + registry.InitializeFilters() + registry.InitializeFacades() + registry.InitializeMiddlewares() + + // Validate Schema Settings + schema.ValidateSchemas() // Start up stores - plugins.StartStores() + registry.StartStores() + + // Start up middlewares + registry.StartMiddlewares() // Start up filters - plugins.StartFilters() + registry.StartFilters() // Start up facades - plugins.StartFacades() + registry.StartFacades() // Wait for goroutines - plugins.WaitForGoroutines() + registry.WaitForGoroutines() // TODO Graceful shutdown } diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 9952c4b..dc54a88 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -1,6 +1,6 @@ package main -import "github.com/ResourceAPI/Core" +import "github.com/StratoAPI/Core" func main() { Core.Run() diff --git a/config.example.json b/config.example.json index fe8d2df..025ff7a 100644 --- a/config.example.json +++ b/config.example.json @@ -1,13 +1,6 @@ { - "host": "0.0.0.0", - "port": 5020, - "plugin_directory": "plugins", + "config_directory": "config", - "database": { - "type": "flatfile", - "flatfile": { - "location": "data.json" - } - } + "default_store": "Flatfile-JSON" } \ No newline at end of file diff --git a/config/config.go b/config/config.go index 22506ac..836d108 100644 --- a/config/config.go +++ b/config/config.go @@ -3,6 +3,8 @@ package config import ( "encoding/json" "fmt" + "github.com/StratoAPI/Core/registry" + "io/ioutil" "os" ) @@ -14,12 +16,53 @@ func InitializeConfig() { panic(err) } + defer configFile.Close() + jsonParser := json.NewDecoder(configFile) if err = jsonParser.Decode(&config); err != nil { panic(err) } - fmt.Println("Config initialized") + if _, err := os.Stat(config.ConfigDirectory); os.IsNotExist(err) { + panic(err) + } + + fmt.Println("Configs initialized") +} + +func InitializePluginConfigs() { + configs := registry.GetRegistryInternal().GetConfigs() + + for name, conf := range configs { + configFile := config.ConfigDirectory + "/" + name + ".json" + structure := (*conf).CreateStructure() + if _, err := os.Stat(configFile); os.IsNotExist(err) { + bytes, err := json.MarshalIndent(structure, "", " ") + if err != nil { + panic(err) + } + + err = ioutil.WriteFile(configFile, bytes, 0644) + if err != nil { + panic(err) + } + } else { + configFile, err := os.Open(configFile) + if err != nil { + panic(err) + } + + jsonParser := json.NewDecoder(configFile) + if err = jsonParser.Decode(&structure); err != nil { + panic(err) + } + + configFile.Close() + } + (*conf).Set(structure) + } + + fmt.Println("Plugin Configs initialized") } func Get() *Config { diff --git a/config/types.go b/config/types.go index dfc6cd4..ea299f9 100644 --- a/config/types.go +++ b/config/types.go @@ -1,19 +1,8 @@ package config type Config struct { - Host string `json:"host"` - Port uint32 `json:"port"` - PluginDirectory string `json:"plugin_directory"` + ConfigDirectory string `json:"config_directory"` - Database Database `json:"database"` -} - -type Database struct { - Type string `json:"type"` - Flatfile Flatfile `json:"flatfile"` -} - -type Flatfile struct { - Location string `json:"location"` + DefaultStore string `json:"default_store"` } diff --git a/filter/filter.go b/filter/filter.go new file mode 100644 index 0000000..bcee21a --- /dev/null +++ b/filter/filter.go @@ -0,0 +1,45 @@ +package filter + +import ( + "errors" + "github.com/StratoAPI/Core/registry" + "github.com/StratoAPI/Interface/filter" +) + +type CoreProcessor struct { +} + +var coreProcessor *CoreProcessor + +func InitializeFilters() { + coreProcessor = &CoreProcessor{} + filter.SetProcessor(coreProcessor) + registerSimpleFilter() +} + +func (cp CoreProcessor) FilterExists(filter string) bool { + return registry.GetRegistryInternal().GetFilter(filter) != nil +} + +func (cp CoreProcessor) CreateFilter(filter string) interface{} { + f := registry.GetRegistryInternal().GetFilter(filter) + if f == nil { + return nil + } + + created, err := (*f).CreateFilter(filter) + + if err != nil { + panic(err) + } + + return created +} + +func (cp CoreProcessor) ValidateFilter(filter filter.ProcessedFilter) (bool, error) { + f := registry.GetRegistryInternal().GetFilter(filter.Type) + if f == nil { + return false, errors.New("filter not found") + } + return (*f).ValidateFilter(filter) +} diff --git a/filter/simple.go b/filter/simple.go new file mode 100644 index 0000000..31e9dd5 --- /dev/null +++ b/filter/simple.go @@ -0,0 +1,63 @@ +package filter + +import ( + "errors" + "github.com/StratoAPI/Core/registry" + "github.com/StratoAPI/Interface/filter" + "reflect" +) + +func registerSimpleFilter() { + registry.GetRegistryInternal().RegisterFilter("simple", &SimpleFilter{}) +} + +type SimpleFilter struct { +} + +func (sf SimpleFilter) Initialize() error { + return nil +} + +func (sf SimpleFilter) Start() error { + return nil +} + +func (sf SimpleFilter) Stop() error { + return nil +} + +func (sf SimpleFilter) ValidateFilter(processed filter.ProcessedFilter) (bool, error) { + simple, ok := processed.Data.(*filter.Simple) + + if !ok { + return false, errors.New("data does not match filter type") + } + + if !simple.Operation.Valid() { + return false, errors.New("filter operation not valid") + } + + if simple.Operation != filter.OpEQ && simple.Operation != filter.OpNEQ { + k := reflect.ValueOf(simple.Value).Type().Kind() + if k == reflect.Invalid || + k == reflect.Bool || + k == reflect.Array || + k == reflect.Chan || + k == reflect.Func || + k == reflect.Interface || + k == reflect.Map || + k == reflect.Ptr || + k == reflect.Slice || + k == reflect.String || + k == reflect.Struct || + k == reflect.UnsafePointer { + return false, errors.New("filter operation can only be applied to numbers") + } + } + + return true, nil +} + +func (sf SimpleFilter) CreateFilter(_ string) (interface{}, error) { + return &filter.Simple{}, nil +} diff --git a/go.mod b/go.mod index c34f7ba..8d18bb7 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,8 @@ -module github.com/ResourceAPI/Core +module github.com/StratoAPI/Core require ( - github.com/Vilsol/GoLib v0.0.7 + github.com/StratoAPI/Interface v0.0.12 github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gorilla/handlers v1.4.0 - github.com/gorilla/mux v1.6.2 github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/testify v1.2.2 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect diff --git a/go.sum b/go.sum index b724fac..f04f4f1 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,7 @@ -github.com/Vilsol/GoLib v0.0.7 h1:zG0skAT/4qPwA/ohDR2KJ90SR8JAfbtZSkq/Lox2oPE= -github.com/Vilsol/GoLib v0.0.7/go.mod h1:6c39n5wTtt5OSt8vEF1mlnc/lC+Xl0SlHG3vboC2pEQ= +github.com/StratoAPI/Interface v0.0.12 h1:cs7sy1isE7clYSoZWBeRuXsLRnX3eYLdBMpziRUVMM4= +github.com/StratoAPI/Interface v0.0.12/go.mod h1:jLlD9bBfAuFE0VN6FkrvX3cMhuDnbikzonCMprOLsyw= 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/gorilla/context v0.0.0-20160226214623-1ea25387ff6f h1:9oNbS1z4rVpbnkHBdPZU4jo9bSmrLpII768arSyMFgk= -github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/handlers v1.4.0 h1:XulKRWSQK5uChr4pEgSE4Tc/OcmnU9GJuSwdog/tZsA= -github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= -github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= diff --git a/middleware/middleware.go b/middleware/middleware.go new file mode 100644 index 0000000..6f8ab28 --- /dev/null +++ b/middleware/middleware.go @@ -0,0 +1,55 @@ +package middleware + +import ( + "github.com/StratoAPI/Core/registry" + "github.com/StratoAPI/Interface/middleware" + "github.com/StratoAPI/Interface/schema" +) + +type CoreProcessor struct { +} + +var coreProcessor *CoreProcessor + +func InitializeMiddleware() { + coreProcessor = &CoreProcessor{} + middleware.SetProcessor(coreProcessor) +} + +func (cp CoreProcessor) Request(resource string, headers map[string][]string) *middleware.RequestResponse { + middlewares := schema.GetProcessor().GetSchema(resource).Meta.Middlewares + + for _, mwMeta := range middlewares { + mw := registry.GetRegistryInternal().GetMiddleware(mwMeta.Type) + if mw == nil { + panic("Middleware " + mwMeta.Type + " is nil") + } + + request := (*mw).Request(resource, headers, mwMeta.Data) + if request != nil { + return request + } + } + + return nil +} + +func (cp CoreProcessor) Response(resource string, headers map[string][]string, data []map[string]interface{}) ([]map[string]interface{}, *middleware.RequestResponse) { + middlewares := schema.GetProcessor().GetSchema(resource).Meta.Middlewares + + for _, mwMeta := range middlewares { + mw := registry.GetRegistryInternal().GetMiddleware(mwMeta.Type) + if mw == nil { + panic("Middleware " + mwMeta.Type + " is nil") + } + + result, response := (*mw).Response(resource, headers, data, mwMeta.Data) + if response != nil { + return nil, response + } + + data = result + } + + return data, nil +} diff --git a/plugins/manager.go b/plugins/manager.go deleted file mode 100644 index bf05c03..0000000 --- a/plugins/manager.go +++ /dev/null @@ -1,87 +0,0 @@ -package plugins - -import ( - "sync" -) - -var storageWaitGroup sync.WaitGroup -var facadeWaitGroup sync.WaitGroup -var filtersWaitGroup sync.WaitGroup - -func InitializeStores() { - for _, store := range stores { - (*store).Initialize() - } -} - -func StartStores() { - storageWaitGroup.Add(len(stores)) - - for _, store := range stores { - go func() { - defer storageWaitGroup.Done() - (*store).Start() - }() - } -} - -func StopStores() { - // TODO 30s timeout - for _, store := range stores { - (*store).Stop() - } -} - -func InitializeFacades() { - for _, facade := range facades { - (*facade).Initialize() - } -} - -func StartFacades() { - facadeWaitGroup.Add(len(facades)) - - for _, facade := range facades { - go func() { - defer facadeWaitGroup.Done() - (*facade).Start() - }() - } -} - -func StopFacades() { - // TODO 30s timeout - for _, facade := range facades { - (*facade).Stop() - } -} - -func InitializeFilters() { - for _, filter := range filters { - (*filter).Initialize() - } -} - -func StartFilters() { - filtersWaitGroup.Add(len(facades)) - - for _, filter := range filters { - go func() { - defer filtersWaitGroup.Done() - (*filter).Start() - }() - } -} - -func StopFilters() { - // TODO 30s timeout - for _, filter := range filters { - (*filter).Stop() - } -} - -func WaitForGoroutines() { - facadeWaitGroup.Wait() - storageWaitGroup.Wait() - filtersWaitGroup.Wait() -} diff --git a/plugins/registry.go b/plugins/registry.go deleted file mode 100644 index 2bc252f..0000000 --- a/plugins/registry.go +++ /dev/null @@ -1,132 +0,0 @@ -package plugins - -import ( - "fmt" - "io/ioutil" - "plugin" - - "github.com/ResourceAPI/Core/config" -) - -var facades map[string]*Facade -var stores map[string]*Storage -var filters map[string]*Filter -var associates map[string][]string - -func InitializePlugins() { - facades = make(map[string]*Facade) - stores = make(map[string]*Storage) - filters = make(map[string]*Filter) - associates = make(map[string][]string) - - files, err := ioutil.ReadDir(config.Get().PluginDirectory) - - if err != nil { - panic(err) - } - - loadedPlugins := make([]Plugin, 0) - for _, f := range files { - plug, err := plugin.Open(config.Get().PluginDirectory + "/" + f.Name()) - if err != nil { - fmt.Println(err) - continue - } - - entrypoint, err := plug.Lookup("CorePlugin") - if err != nil { - fmt.Println(err) - continue - } - - var pl Plugin - pl, ok := entrypoint.(Plugin) - if !ok { - fmt.Println("unexpected type from module symbol") - continue - } - - pl.Entrypoint() - loadedPlugins = append(loadedPlugins, pl) - } - - pluginNames := make([]string, 0) - facadeNames := make([]string, 0) - storageNames := make([]string, 0) - filterNames := make([]string, 0) - - for _, v := range loadedPlugins { - pluginNames = append(pluginNames, v.Name()) - } - - for k := range facades { - facadeNames = append(facadeNames, k) - } - - for k := range stores { - storageNames = append(storageNames, k) - } - - for k := range filters { - filterNames = append(filterNames, k) - } - - fmt.Printf("Loaded %d plugin(s): %+v\n", len(loadedPlugins), pluginNames) - fmt.Printf("Loaded %d facade(s): %+v\n", len(facades), facadeNames) - fmt.Printf("Loaded %d storage(s): %+v\n", len(stores), storageNames) - fmt.Printf("Loaded %d filter(s): %+v\n", len(filters), filterNames) - fmt.Printf("Loaded %d filter association(s)\n", len(associates)) -} - -func RegisterFacade(name string, facade Facade) error { - if _, ok := facades[name]; ok { - panic("Facade with name " + name + " is already registered!") - } - - facades[name] = &facade - - return nil -} - -func RegisterStorage(name string, storage Storage) error { - if _, ok := stores[name]; ok { - panic("Storage with name " + name + " is already registered!") - } - - stores[name] = &storage - - return nil -} - -func RegisterFilter(name string, filter Filter) error { - if _, ok := filters[name]; ok { - panic("Filter with name " + name + " is already registered!") - } - - filters[name] = &filter - - return nil -} - -func AssociateFilter(filter string, storage string) error { - if _, ok := associates[filter]; !ok { - associates[filter] = make([]string, 0) - } - - supportedStores := associates[filter] - - for _, store := range supportedStores { - if store == storage { - panic("Filter " + filter + " is already associated with storage " + storage + "!") - } - } - - supportedStores = append(supportedStores, storage) - associates[filter] = supportedStores - - return nil -} - -func GetStore(store string) *Storage { - return stores[store] -} diff --git a/plugins/types.go b/plugins/types.go deleted file mode 100644 index ec81a57..0000000 --- a/plugins/types.go +++ /dev/null @@ -1,54 +0,0 @@ -package plugins - -type Plugin interface { - Name() string - Entrypoint() -} - -type Facade interface { - // Initialize the facade. - Initialize() error - - // Start the facade. Must be a blocking call. - Start() error - - // Graceful stopping of the facade with a 30s timeout. - Stop() error -} - -type Storage interface { - // Initialize the storage. - Initialize() error - - // Start the storage. Must be a blocking call. - Start() error - - // Graceful stopping of the storage with a 30s timeout. - Stop() error - - // Retrieve resources. - GetResources(resource string, filters []interface{}) ([]map[string]interface{}, error) - - // Create resources. - CreateResources(resource string, data []map[string]interface{}) error - - // Update resources. - UpdateResources(resource string, data []map[string]interface{}, filters []interface{}) error - - // Delete resources. - DeleteResources(resource string, filters []interface{}) error -} - -type Filter interface { - // Initialize the filter. - Initialize() error - - // Start the filter. Does not have to be blocking. - Start() error - - // Graceful stopping of the filter with a 30s timeout. - Stop() error - - // Validate structure for filter validness - ValidateFilter(filter interface{}) (bool, error) -} diff --git a/registry/manager.go b/registry/manager.go new file mode 100644 index 0000000..fc8f5a4 --- /dev/null +++ b/registry/manager.go @@ -0,0 +1,188 @@ +package registry + +import ( + "fmt" + "os" + "os/signal" + "sync" + "time" +) + +var storageWaitGroup sync.WaitGroup +var facadeWaitGroup sync.WaitGroup +var filtersWaitGroup sync.WaitGroup +var middlewareWaitGroup sync.WaitGroup + +func InitializeStores() { + for _, store := range coreRegistry.stores { + err := (*store).Initialize() + + if err != nil { + panic(err) + } + } +} + +func StartStores() { + storageWaitGroup.Add(len(coreRegistry.stores)) + + for _, store := range coreRegistry.stores { + go func() { + defer storageWaitGroup.Done() + err := (*store).Start() + + if err != nil { + panic(err) + } + }() + } +} + +func StopStores() { + for _, store := range coreRegistry.stores { + err := (*store).Stop() + + if err != nil { + panic(err) + } + } +} + +func InitializeFacades() { + for _, facade := range coreRegistry.facades { + err := (*facade).Initialize() + + if err != nil { + panic(err) + } + } +} + +func StartFacades() { + facadeWaitGroup.Add(len(coreRegistry.facades)) + + for _, facade := range coreRegistry.facades { + go func() { + defer facadeWaitGroup.Done() + err := (*facade).Start() + + if err != nil { + panic(err) + } + }() + } +} + +func StopFacades() { + for _, facade := range coreRegistry.facades { + err := (*facade).Stop() + + if err != nil { + panic(err) + } + } +} + +func InitializeFilters() { + for _, filter := range coreRegistry.filters { + err := (*filter).Initialize() + + if err != nil { + panic(err) + } + } +} + +func StartFilters() { + filtersWaitGroup.Add(len(coreRegistry.facades)) + + for _, filter := range coreRegistry.filters { + go func() { + defer filtersWaitGroup.Done() + err := (*filter).Start() + + if err != nil { + panic(err) + } + }() + } +} + +func StopFilters() { + for _, filter := range coreRegistry.filters { + err := (*filter).Stop() + + if err != nil { + panic(err) + } + } +} + +func InitializeMiddlewares() { + for _, middleware := range coreRegistry.middlewares { + err := (*middleware).Initialize() + + if err != nil { + panic(err) + } + } +} + +func StartMiddlewares() { + storageWaitGroup.Add(len(coreRegistry.middlewares)) + + for _, middleware := range coreRegistry.middlewares { + go func() { + defer storageWaitGroup.Done() + err := (*middleware).Start() + + if err != nil { + panic(err) + } + }() + } +} + +func StopMiddlewares() { + for _, middleware := range coreRegistry.middlewares { + err := (*middleware).Stop() + + if err != nil { + panic(err) + } + } +} + +func WaitForGoroutines() { + done := make(chan bool, 1) + + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt) + + go func() { + facadeWaitGroup.Wait() + storageWaitGroup.Wait() + filtersWaitGroup.Wait() + middlewareWaitGroup.Wait() + done <- true + }() + + <-stop + + fmt.Println("Shutting down the server...") + + go func() { + StopFacades() + StopStores() + StopFilters() + StopMiddlewares() + }() + + select { + // TODO Make time configurable + case <-time.After(30 * time.Second): + panic("Graceful shutdown failed!") + case <-done: + fmt.Println("Server shut down") + } +} diff --git a/registry/registry.go b/registry/registry.go new file mode 100644 index 0000000..71e6775 --- /dev/null +++ b/registry/registry.go @@ -0,0 +1,186 @@ +package registry + +import ( + "fmt" + "io/ioutil" + "plugin" + + "github.com/StratoAPI/Interface/plugins" +) + +type CoreRegistry struct { + facades map[string]*plugins.Facade + stores map[string]*plugins.Storage + filters map[string]*plugins.Filter + middlewares map[string]*plugins.Middleware + configs map[string]*plugins.Config + associates map[string][]string +} + +var coreRegistry *CoreRegistry + +func GetRegistryInternal() *CoreRegistry { + return coreRegistry +} + +func InitializeRegistry() { + coreRegistry = &CoreRegistry{ + facades: make(map[string]*plugins.Facade), + stores: make(map[string]*plugins.Storage), + filters: make(map[string]*plugins.Filter), + middlewares: make(map[string]*plugins.Middleware), + configs: make(map[string]*plugins.Config), + associates: make(map[string][]string), + } +} + +func InitializePlugins(pluginDirectory string) { + files, err := ioutil.ReadDir(pluginDirectory) + + if err != nil { + panic(err) + } + + plugins.SetRegistry(coreRegistry) + + loadedPlugins := make([]plugins.Plugin, 0) + for _, f := range files { + plug, err := plugin.Open(pluginDirectory + "/" + f.Name()) + if err != nil { + fmt.Println(err) + continue + } + + entrypoint, err := plug.Lookup("CorePlugin") + if err != nil { + fmt.Println(err) + continue + } + + var pl plugins.Plugin + pl, ok := entrypoint.(plugins.Plugin) + if !ok { + fmt.Println("unexpected type from module symbol") + continue + } + + pl.Entrypoint() + loadedPlugins = append(loadedPlugins, pl) + } + + pluginNames := make([]string, 0) + facadeNames := make([]string, 0) + storageNames := make([]string, 0) + filterNames := make([]string, 0) + + for _, v := range loadedPlugins { + pluginNames = append(pluginNames, v.Name()) + } + + for k := range coreRegistry.facades { + facadeNames = append(facadeNames, k) + } + + for k := range coreRegistry.stores { + storageNames = append(storageNames, k) + } + + for k := range coreRegistry.filters { + filterNames = append(filterNames, k) + } + + fmt.Printf("Loaded %d plugin(s): %+v\n", len(loadedPlugins), pluginNames) + fmt.Printf("Loaded %d facade(s): %+v\n", len(coreRegistry.facades), facadeNames) + fmt.Printf("Loaded %d storage(s): %+v\n", len(coreRegistry.stores), storageNames) + fmt.Printf("Loaded %d filter(s): %+v\n", len(coreRegistry.filters), filterNames) + fmt.Printf("Loaded %d filter association(s)\n", len(coreRegistry.associates)) +} + +func (cr CoreRegistry) RegisterFacade(name string, facade plugins.Facade) error { + if _, ok := cr.facades[name]; ok { + panic("Facade with name " + name + " is already registered!") + } + + cr.facades[name] = &facade + + return nil +} + +func (cr CoreRegistry) RegisterStorage(name string, storage plugins.Storage) error { + if _, ok := cr.stores[name]; ok { + panic("Storage with name " + name + " is already registered!") + } + + cr.stores[name] = &storage + + return nil +} + +func (cr CoreRegistry) RegisterFilter(name string, filter plugins.Filter) error { + if _, ok := cr.filters[name]; ok { + panic("Filter with name " + name + " is already registered!") + } + + cr.filters[name] = &filter + + return nil +} + +func (cr CoreRegistry) RegisterMiddleware(name string, middleware plugins.Middleware) error { + if _, ok := cr.middlewares[name]; ok { + panic("Middleware with name " + name + " is already registered!") + } + + cr.middlewares[name] = &middleware + + return nil +} + +func (cr CoreRegistry) RegisterConfig(name string, config plugins.Config) error { + if _, ok := cr.configs[name]; ok { + panic("Config with name " + name + " is already registered!") + } + + cr.configs[name] = &config + + return nil +} + +func (cr CoreRegistry) AssociateFilter(filter string, storage string) error { + if _, ok := cr.associates[filter]; !ok { + cr.associates[filter] = make([]string, 0) + } + + supportedStores := cr.associates[filter] + + for _, store := range supportedStores { + if store == storage { + panic("Filter " + filter + " is already associated with storage " + storage + "!") + } + } + + supportedStores = append(supportedStores, storage) + cr.associates[filter] = supportedStores + + return nil +} + +func (cr CoreRegistry) GetStore(store string) *plugins.Storage { + return cr.stores[store] +} + +func (cr CoreRegistry) GetFilter(filter string) *plugins.Filter { + return cr.filters[filter] +} + +func (cr CoreRegistry) GetAssociates(filter string) []string { + return cr.associates[filter] +} + +func (cr CoreRegistry) GetMiddleware(middleware string) *plugins.Middleware { + return cr.middlewares[middleware] +} + +func (cr CoreRegistry) GetConfigs() map[string]*plugins.Config { + return cr.configs +} diff --git a/resource/resource.go b/resource/resource.go index 7d0016b..dd1686d 100644 --- a/resource/resource.go +++ b/resource/resource.go @@ -1,15 +1,106 @@ package resource import ( - "github.com/ResourceAPI/Core/plugins" - "github.com/ResourceAPI/Core/schema" + "errors" + "github.com/StratoAPI/Core/config" + "github.com/StratoAPI/Core/registry" + "github.com/StratoAPI/Interface/filter" + "github.com/StratoAPI/Interface/plugins" + "github.com/StratoAPI/Interface/resource" + "github.com/StratoAPI/Interface/schema" ) -func GetStore(resource string) *plugins.Storage { - return plugins.GetStore(schema.GetSchema(resource).GetStore()) +type CoreProcessor struct { } -func GetResources() []string { +var coreProcessor *CoreProcessor + +func InitializeResources() { + coreProcessor = &CoreProcessor{} + resource.SetProcessor(coreProcessor) +} + +func (cp CoreProcessor) GetStoreName(resource string) string { + resourceStore := schema.GetProcessor().GetSchema(resource).Meta.Store + + if resourceStore == "" { + resourceStore = config.Get().DefaultStore + } + + return resourceStore +} + +func (cp CoreProcessor) GetStore(resource string) *plugins.Storage { + return registry.GetRegistryInternal().GetStore(cp.GetStoreName(resource)) +} + +func (cp CoreProcessor) GetResourceList() []string { // TODO return []string{} } + +func (cp CoreProcessor) GetResources(resource string, filters []filter.ProcessedFilter) ([]map[string]interface{}, error) { + resourceStore := cp.GetStoreName(resource) + + err := checkStoreFilterAssociates(resourceStore, filters) + + if err != nil { + return nil, err + } + + return (*registry.GetRegistryInternal().GetStore(resourceStore)).GetResources(resource, filters) +} + +func (cp CoreProcessor) CreateResources(resource string, data []map[string]interface{}) error { + return (*cp.GetStore(resource)).CreateResources(resource, data) +} + +func (cp CoreProcessor) UpdateResources(resource string, data map[string]interface{}, filters []filter.ProcessedFilter) error { + resourceStore := cp.GetStoreName(resource) + + err := checkStoreFilterAssociates(resourceStore, filters) + + if err != nil { + return err + } + + return (*registry.GetRegistryInternal().GetStore(resourceStore)).UpdateResources(resource, data, filters) +} + +func (cp CoreProcessor) DeleteResources(resource string, filters []filter.ProcessedFilter) error { + resourceStore := cp.GetStoreName(resource) + + err := checkStoreFilterAssociates(resourceStore, filters) + + if err != nil { + return err + } + + return (*registry.GetRegistryInternal().GetStore(resourceStore)).DeleteResources(resource, filters) +} + +func checkStoreFilterAssociates(resourceStore string, filters []filter.ProcessedFilter) error { + for _, f := range filters { + associates := registry.GetRegistryInternal().GetAssociates(f.Type) + + if len(associates) == 0 { + return errors.New("the store does not support a provided filter") + } + + found := false + for _, store := range associates { + if store == resourceStore { + found = true + break + } + } + + if found { + break + } + + return errors.New("the store does not support a provided filter") + } + + return nil +} diff --git a/schema/schema.go b/schema/schema.go index 200ce30..e3caf4e 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -4,19 +4,27 @@ import ( "encoding/json" "errors" "fmt" + "github.com/StratoAPI/Core/config" + "github.com/StratoAPI/Core/registry" "io/ioutil" "os" "strings" + schemaInt "github.com/StratoAPI/Interface/schema" + "github.com/xeipuuv/gojsonschema" ) -type Schema struct { - Data gojsonschema.JSONLoader - Source map[string]interface{} +type CoreProcessor struct { + schemas map[string]CoreSchema } -var schemas = make(map[string]Schema) +var coreProcessor *CoreProcessor + +type CoreSchema struct { + Parent schemaInt.Schema + Data *gojsonschema.Schema +} func InitializeSchemas() { files, err := ioutil.ReadDir("./resources") @@ -25,39 +33,99 @@ func InitializeSchemas() { panic(err) } + coreProcessor = &CoreProcessor{ + schemas: make(map[string]CoreSchema), + } + + schemaInt.SetProcessor(coreProcessor) + for _, f := range files { if strings.HasSuffix(f.Name(), ".json") { bytes, _ := ioutil.ReadFile("./resources/" + f.Name()) schema := gojsonschema.NewBytesLoader(bytes) - var source interface{} - json.Unmarshal(bytes, &source) + var source map[string]interface{} + err := json.Unmarshal(bytes, &source) + + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to load resource %s:\n", f.Name()) + fmt.Fprintln(os.Stderr, err) + continue + } + + if _, ok := source["meta"]; !ok { + fmt.Fprintf(os.Stderr, "Failed to load resource %s:\n", f.Name()) + fmt.Fprintln(os.Stderr, "resource does not contain meta object") + continue + } - s := Schema{ - Data: schema, - Source: source.(map[string]interface{}), + temp, _ := json.Marshal(source["meta"]) + meta := new(schemaInt.ResourceMeta) + err = json.Unmarshal(temp, &meta) + + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to load resource %s:\n", f.Name()) + fmt.Fprintln(os.Stderr, err) + continue + } + + newSchema, err := gojsonschema.NewSchema(schema) + + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to load compile resource schema %s:\n", f.Name()) + fmt.Fprintln(os.Stderr, err) + continue + } + + s := CoreSchema{ + Data: newSchema, + Parent: schemaInt.Schema{ + Source: source, + Meta: *meta, + }, } - resource, err := s.GetResource() if err != nil { fmt.Fprintf(os.Stderr, "Failed to load resource %s:\n", f.Name()) fmt.Fprintln(os.Stderr, err) continue } - schemas[resource] = s + coreProcessor.schemas[meta.Resource] = s } } - fmt.Printf("Loaded %d schema(s)\n", len(schemas)) + fmt.Printf("Loaded %d schema(s)\n", len(coreProcessor.schemas)) } -func ResourceExists(resource string) bool { - _, ok := schemas[resource] +func ValidateSchemas() { + for _, schema := range coreProcessor.schemas { + selectedStore := schema.Parent.Meta.Store + + if selectedStore == "" { + selectedStore = config.Get().DefaultStore + } + + if registry.GetRegistryInternal().GetStore(selectedStore) == nil { + panic("resource " + schema.Parent.Meta.Resource + " uses an unsupported store: " + selectedStore) + } + } +} + +func (cp CoreProcessor) ResourceExists(resource string) bool { + _, ok := cp.schemas[resource] return ok } -func ResourceValid(resource string, data string) (bool, error) { - result, err := gojsonschema.Validate(schemas[resource].Data, gojsonschema.NewStringLoader(data)) +func (cp CoreProcessor) ResourceValid(resource string, data string, required bool) (bool, error) { + return cp.validateSchema(resource, gojsonschema.NewStringLoader(data), required) +} + +func (cp CoreProcessor) ResourceValidGo(resource string, data interface{}, required bool) (bool, error) { + return cp.validateSchema(resource, gojsonschema.NewGoLoader(data), required) +} + +func (cp CoreProcessor) validateSchema(resource string, loader gojsonschema.JSONLoader, required bool) (bool, error) { + result, err := cp.schemas[resource].Data.Validate(loader) if err != nil { return false, err @@ -68,8 +136,16 @@ func ResourceValid(resource string, data string) (bool, error) { } errs := "" + validErrors := false for i, err := range result.Errors() { + if !required && err.Type() == "required" { + continue + } + + validErrors = true + + err.Type() if i > 0 { errs += ", " } @@ -77,46 +153,18 @@ func ResourceValid(resource string, data string) (bool, error) { errs += err.String() } - return false, errors.New(errs) -} - -func GetSchema(resource string) *Schema { - if _, ok := schemas[resource]; !ok { - return nil - } - - schema := schemas[resource] - return &schema -} - -func (schema Schema) GetRaw(key string) (interface{}, error) { - if _, ok := schema.Source[key]; !ok { - return nil, errors.New("key '" + key + "' does not exist in schema") + if !validErrors { + return true, nil } - return schema.Source[key], nil + return false, errors.New(errs) } -func (schema Schema) GetRawString(key string) (string, error) { - raw, err := schema.GetRaw(key) - - if err != nil { - return "", err - } - - casted, ok := raw.(string) - - if !ok { - return "", errors.New("key '" + key + "' is not of type string in schema") +func (cp CoreProcessor) GetSchema(resource string) *schemaInt.Schema { + if _, ok := cp.schemas[resource]; !ok { + return nil } - return casted, nil -} - -func (schema Schema) GetStore() (string, error) { - return schema.GetRawString("store") -} - -func (schema Schema) GetResource() (string, error) { - return schema.GetRawString("resource") + schema := cp.schemas[resource] + return &(schema.Parent) }