├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .golangci.yaml ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── application.go ├── component.go ├── config.go ├── dependency.go ├── docs ├── .gitignore ├── Dockerfile ├── README.md ├── build.sh ├── docker-compose.yml ├── mkdocs.yml ├── mkdocs │ ├── hooks │ │ ├── navigation.py │ │ └── social.py │ └── theme │ │ ├── assets │ │ ├── images │ │ │ ├── favicon.ico │ │ │ ├── gopher-eyes.webp │ │ │ ├── logo.png │ │ │ └── social.png │ │ └── stylesheets │ │ │ ├── extra.css │ │ │ └── home.css │ │ ├── home.html │ │ └── partials │ │ ├── copyright.html │ │ └── meta_social_tags.html ├── pages │ ├── assets │ │ └── images │ │ │ ├── diagrams │ │ │ └── componego-flow.svg │ │ │ └── warnings │ │ │ └── goroutine-leak.png │ ├── contribution │ │ └── guide.md │ ├── get-started.md │ ├── impl │ │ ├── application.md │ │ ├── component.md │ │ ├── config.md │ │ ├── dependency.md │ │ ├── driver.md │ │ ├── environment.md │ │ ├── processor.md │ │ └── runner.md │ ├── index.md │ ├── tests │ │ ├── mock.md │ │ └── runner.md │ └── warnings │ │ └── goroutine-leak.md ├── poetry.lock └── pyproject.toml ├── environment.go ├── examples ├── Dockerfile ├── README.md ├── docker-compose.yml ├── hello-app │ ├── cmd │ │ └── application │ │ │ ├── dev │ │ │ └── main.go │ │ │ └── main.go │ ├── internal │ │ └── application │ │ │ └── application.go │ └── tests │ │ ├── basic_test.go │ │ └── mocks │ │ └── application.go ├── urfave-cli-integration │ └── README.md └── url-shortener-app │ ├── .gitignore │ ├── cmd │ └── application │ │ ├── dev │ │ └── main.go │ │ └── main.go │ ├── config │ └── config.json.example │ ├── internal │ ├── application │ │ └── application.go │ ├── domain │ │ └── domain.go │ ├── migration │ │ └── migration.go │ ├── repository │ │ └── repository.go │ ├── server │ │ ├── handlers │ │ │ ├── index.go │ │ │ └── redirect.go │ │ ├── json │ │ │ └── json.go │ │ └── server.go │ └── utils │ │ └── utils.go │ ├── pkg │ └── components │ │ ├── database │ │ ├── component.go │ │ ├── examples │ │ │ └── config │ │ │ │ └── config.json │ │ ├── internal │ │ │ ├── config.go │ │ │ └── provider.go │ │ └── tests │ │ │ ├── database_test.go │ │ │ └── mocks │ │ │ └── application.go │ │ ├── server │ │ ├── component.go │ │ ├── examples │ │ │ └── config │ │ │ │ └── config.json │ │ └── internal │ │ │ ├── config.go │ │ │ └── server.go │ │ └── test-server │ │ ├── component.go │ │ └── internal │ │ └── server.go │ ├── tests │ ├── integration_test.go │ └── mocks │ │ └── application.go │ └── third_party │ ├── Readme.md │ ├── config-reader │ └── reader.go │ ├── db-driver │ ├── driver.go │ └── queries.go │ ├── errgroup │ └── errgroup.go │ └── servermux │ └── router.go ├── go.mod ├── go.sum ├── impl ├── application │ ├── factory.go │ ├── helpers.go │ ├── io.go │ └── tests │ │ └── helpers_test.go ├── driver │ ├── driver.go │ ├── options.go │ └── tests │ │ ├── driver.go │ │ └── driver_test.go ├── environment │ ├── environment.go │ ├── managers │ │ ├── component │ │ │ ├── factory.go │ │ │ ├── manager.go │ │ │ └── tests │ │ │ │ ├── factory_test.go │ │ │ │ ├── manager.go │ │ │ │ └── manager_test.go │ │ ├── config │ │ │ ├── helper.go │ │ │ └── manager.go │ │ └── dependency │ │ │ ├── container │ │ │ ├── container.go │ │ │ └── tests │ │ │ │ ├── container.go │ │ │ │ └── container_test.go │ │ │ ├── helpers.go │ │ │ ├── manager.go │ │ │ └── tests │ │ │ ├── helper_test.go │ │ │ ├── manager.go │ │ │ └── manager_test.go │ └── tests │ │ ├── environment.go │ │ └── environment_test.go ├── processors │ ├── processor.go │ ├── tests │ │ └── types_test.go │ └── types.go └── runner │ ├── runner.go │ └── unhandled-errors │ ├── handlers │ ├── default.go │ └── vendor-proxy.go │ └── render.go ├── internal ├── developer │ ├── message.go │ └── tests │ │ └── message_test.go ├── system │ ├── os.go │ ├── runtime.go │ └── tests │ │ └── runtime_test.go ├── testing │ ├── logger │ │ ├── logger.go │ │ └── tests │ │ │ └── logger_test.go │ ├── require │ │ ├── call.go │ │ ├── require.go │ │ └── tests │ │ │ └── call_test.go │ ├── testing.go │ └── types │ │ └── types.go └── utils │ ├── context.go │ ├── fprint.go │ ├── reflect.go │ ├── slice.go │ └── tests │ ├── context_test.go │ ├── fprint_test.go │ ├── reflect_test.go │ └── slice_test.go ├── libs ├── color │ ├── color.go │ └── theme.go ├── debug │ ├── stack.go │ ├── tests │ │ └── variable_test.go │ └── variable.go ├── ordered-map │ ├── map.go │ └── tests │ │ ├── map.go │ │ └── map_test.go ├── type-cast │ ├── bool.go │ ├── float.go │ ├── int.go │ ├── string.go │ └── tests │ │ ├── bool_test.go │ │ ├── float_test.go │ │ ├── int_test.go │ │ └── string_test.go ├── vendor-proxy │ ├── proxy.go │ └── tests │ │ ├── proxy.go │ │ └── proxy_test.go └── xerrors │ ├── option.go │ ├── tests │ ├── unwrap_test.go │ ├── xerrors.go │ └── xerrors_test.go │ ├── unwrap.go │ └── xerrors.go ├── processor.go ├── scripts ├── Dockerfile └── make.py ├── tests └── runner │ ├── runner.go │ └── tests │ └── runner_test.go └── tools ├── create-basic-app.sh └── create-contributor-env.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vs-code/ 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [{*.yml,*.yaml}] 12 | indent_size = 2 13 | 14 | [{Makefile,go.mod,go.sum}] 15 | indent_style = tab 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.bat text eol=crlf 3 | *.cmd text eol=crlf 4 | *.ahk text eol=crlf 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea 3 | **/.idea 4 | .vs-code 5 | **/.vs-code 6 | 7 | # MacOS files 8 | .DS_STORE 9 | **/.DS_Store 10 | 11 | # Binaries for programs and plugins 12 | *.exe 13 | *.exe~ 14 | *.dll 15 | *.so 16 | *.dylib 17 | 18 | # Test binary, built with `go test -c` 19 | *.test 20 | 21 | # Output of the go coverage tool, specifically when used with LiteIDE 22 | *.out 23 | 24 | # Dependency directories 25 | vendor/ 26 | 27 | # Python environment 28 | venv/ 29 | __pycache__ 30 | **/__pycache__ 31 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | linters: 4 | enable: 5 | - asciicheck 6 | - copyloopvar 7 | - depguard 8 | - dogsled 9 | - durationcheck 10 | - errcheck 11 | - errorlint 12 | - gci 13 | - gofmt 14 | - goimports 15 | - gosec 16 | - gosimple 17 | - misspell 18 | - nakedret 19 | - nilerr 20 | - nolintlint 21 | - revive 22 | - staticcheck 23 | - unparam 24 | - unused 25 | - wastedassign 26 | issues: 27 | exclude-rules: 28 | - linters: 29 | - revive 30 | text: 'var-naming:' 31 | - linters: 32 | - goimports 33 | text: File is not `goimports`-ed 34 | linters-settings: 35 | depguard: 36 | rules: 37 | main: 38 | allow: 39 | - $gostd 40 | - github.com/unpleasantcam/componego 41 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: 'https://github.com/pre-commit/pre-commit-hooks' 3 | rev: v4.6.0 4 | hooks: 5 | - id: end-of-file-fixer 6 | - id: trailing-whitespace 7 | - id: check-added-large-files 8 | args: 9 | - '--maxkb=200' 10 | - repo: local 11 | hooks: 12 | - id: componego-framework-development 13 | name: Componego Framework Development Hook 14 | entry: 'python ./scripts/make.py commit:hook' 15 | language: python 16 | pass_filenames: false 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.0 (September 26, 2024) 2 | 3 | * Improved documentation and testing. 4 | * Fixed some bugs. 5 | 6 | ## 0.1.0 (July 27, 2024) 7 | 8 | * All interfaces are stable now and will no longer be changed. 9 | * Improved the graceful shutdown. 10 | * Fixed dependency closing order. 11 | * Increased GitHub Action speed. 12 | 13 | ## 0.0.3 (July 8, 2024) 14 | 15 | * The graceful shutdown component was replaced with various application launch functions. 16 | * Added the ability to conveniently close resources in dependencies. 17 | * Improved the xerrors package and added error codes. 18 | * Added strict code formatting. 19 | 20 | ## 0.0.2 (June 30, 2024) 21 | 22 | * Increased test coverage. 23 | * Improved dependency injection stability. 24 | * Improved several interfaces for flexibility. 25 | * Added linking of application skeleton creation to the last git tag. 26 | 27 | ## 0.0.1 (June 1, 2024) 28 | 29 | * Initial release. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | fmt: 2 | python ./scripts/make.py fmt 3 | 4 | tests: 5 | python ./scripts/make.py tests 6 | 7 | tests-cover: 8 | python ./scripts/make.py tests:cover 9 | 10 | lint: 11 | python ./scripts/make.py lint 12 | 13 | security: 14 | python ./scripts/make.py security 15 | 16 | generate: 17 | python ./scripts/make.py generate 18 | 19 | all: fmt tests tests-cover lint security generate 20 | 21 | .NOTPARALLEL: 22 | .PHONY: all fmt tests tests-cover lint security generate 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ComponeGo Framework 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/unpleasantcam/componego)](https://goreportcard.com/report/github.com/unpleasantcam/componego) 4 | [![Tests](https://github.com/unpleasantcam/componego/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/unpleasantcam/componego/actions/workflows/tests.yml) 5 | [![codecov](https://codecov.io/gh/componego/componego/branch/master/graph/badge.svg?token=W4CPM75389)](https://codecov.io/gh/componego/componego) 6 | [![Go Reference](https://pkg.go.dev/badge/github.com/unpleasantcam/componego.svg)](https://pkg.go.dev/github.com/unpleasantcam/componego) 7 | [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go) 8 | 9 | ![screenshot](./docs/mkdocs/theme/assets/images/social.png) 10 | 11 | It is a framework for building applications based on components. These components can be used in multiple applications and are interchangeable. 12 | This framework is used solely to initialize the application and does NOT affect the main loop of your application. 13 | You can still use your favorite frameworks and libraries. We allow you to wrap them in components. 14 | 15 | Components may depend on other components. They can be expanded or reduced based on your requirements. 16 | Components are not microservices; they are folders that contain different functionalities. 17 | 18 | The framework has very low coupling within its code. All entities are optional. 19 | 20 | We provide the ability to use dependency injection, configuration, and error handling. 21 | However, one of the framework's main features is that you can modify entities without changing the application code. 22 | This allows you to create mocks for any part of your application without changing the code. 23 | 24 | If your application is divided into components (modules), it further separates your code into different services and allows you to reuse it in other applications. 25 | Of course, you don’t need to make components too small. 26 | 27 | ### Documentation 28 | 29 | Introduction: [medium.com/@konstanchuk/25bfd16a97a9](https://medium.com/@konstanchuk/25bfd16a97a9). 30 | 31 | Visit our website to learn more: [componego.github.io](https://componego.github.io/). 32 | 33 | The documentation is up-to-date with the latest version of the framework. 34 | Please update your version to [the latest](https://github.com/unpleasantcam/componego/releases). 35 | 36 | ### Examples 37 | 38 | You can find some examples [here](./examples). 39 | 40 | A typical application of this framework looks like [this](./examples/url-shortener-app/internal/application/application.go). 41 | 42 | ### Skeleton 43 | 44 | You can quickly create a basic application in several ways: 45 | ```shell 46 | curl -sSL https://raw.githubusercontent.com/componego/componego/master/tools/create-basic-app.sh | sh 47 | ``` 48 | or 49 | ```shell 50 | wget -O - https://raw.githubusercontent.com/componego/componego/master/tools/create-basic-app.sh | sh 51 | ``` 52 | On Windows, you can run the commands above with Git Bash, which comes with [Git for Windows](https://git-scm.com/download/win). 53 | 54 | ### Contributing 55 | 56 | We are open to improvements and suggestions. Pull requests are welcome. 57 | 58 | ### License 59 | 60 | The source code of the repository is licensed under the [Apache 2.0 license](./LICENSE). 61 | The core of the framework does not depend on other packages. 62 | -------------------------------------------------------------------------------- /component.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package componego 18 | 19 | // Component is an interface that describes the component. 20 | type Component interface { 21 | // ComponentIdentifier returns the component ID. 22 | // If the identifier in several components is the same, then the last component will be used. 23 | // This can be used to overwrite components. 24 | ComponentIdentifier() string 25 | // ComponentVersion returns the component version. 26 | ComponentVersion() string 27 | } 28 | 29 | // ComponentComponents is an interface that describes which components the current component depends on. 30 | type ComponentComponents interface { 31 | // Component belongs to the component. 32 | Component 33 | // ComponentComponents returns list of components that the component depends on. 34 | // They will be sorted based on the dependent components. 35 | ComponentComponents() ([]Component, error) 36 | } 37 | 38 | // ComponentDependencies is an interface that describes the dependencies of the component. 39 | type ComponentDependencies interface { 40 | // Component belongs to the component. 41 | Component 42 | // ComponentDependencies returns a list of dependencies that the component provides 43 | // This function must return an array of objects or functions that returns objects. 44 | ComponentDependencies() ([]Dependency, error) 45 | } 46 | 47 | // ComponentInit is an interface that describes the component initialization. 48 | type ComponentInit interface { 49 | // Component belongs to the component. 50 | Component 51 | // ComponentInit is called during component initialization. 52 | ComponentInit(env Environment) error 53 | } 54 | 55 | // ComponentStop is an interface that describes stopping the component. 56 | type ComponentStop interface { 57 | // Component belongs to the component. 58 | Component 59 | // ComponentStop is called when the component stops. 60 | // You can handle previous error (return a new or old error). 61 | ComponentStop(env Environment, prevErr error) error 62 | } 63 | 64 | // ComponentProvider is an interface that describes a list of active application components. 65 | // These components are sorted in order of dependencies between components. 66 | type ComponentProvider interface { 67 | // Components returns a list of sorted application components. 68 | Components() []Component 69 | } 70 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package componego 18 | 19 | // ConfigProvider is an interface that describes configuration getter. 20 | type ConfigProvider interface { 21 | // ConfigValue returns the configuration by key. 22 | // The value can be modified by the Processor which validates or converts the value. 23 | ConfigValue(configKey string, processor Processor) (any, error) 24 | } 25 | -------------------------------------------------------------------------------- /dependency.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package componego 18 | 19 | type Dependency any 20 | 21 | // DependencyInvoker is an interface that describes the functions that invoke dependencies for the application. 22 | type DependencyInvoker interface { 23 | // Invoke uses dependencies. 24 | // You can only pass a function as an argument. 25 | // If the passed function returns an error, then it will be returned. 26 | Invoke(function any) (any, error) 27 | // Populate is similar to Invoke, but it can only take objects that are a reference. 28 | // The passed object will be filled with dependencies. 29 | Populate(target any) error 30 | // PopulateFields fills the struct fields passed as an argument with dependencies. 31 | // Fields must have a special tag: `componego:"inject"`. 32 | // Fields without this tag are ignored. 33 | PopulateFields(target any) error 34 | } 35 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | build/ 3 | -------------------------------------------------------------------------------- /docs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10.13-slim 2 | 3 | WORKDIR /docs 4 | 5 | # mkdocs mkdocs-material mkdocs-minify-plugin mkdocs-material-extensions beautifulsoup4 6 | COPY pyproject.toml poetry.lock ./ 7 | 8 | RUN pip install poetry==1.7.0 && poetry install --no-root --no-directory 9 | 10 | COPY . . 11 | RUN poetry install 12 | 13 | EXPOSE 8123 14 | ENTRYPOINT ["poetry", "run", "mkdocs", "serve", "--dev-addr=0.0.0.0:8123"] 15 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | Please visit the [website](https://componego.github.io) to see the documentation, or run the documentation locally using the command: '**docker-compose up componego-framework-docs**'. 2 | 3 | You can also use previous commits to run a local website with documentation for earlier versions of the framework. 4 | -------------------------------------------------------------------------------- /docs/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | cd "$(dirname "$(realpath -- "$0")")"; 7 | 8 | docker-compose run --entrypoint "poetry run mkdocs build --clean --site-dir=/docs/build" componego-framework-docs 9 | 10 | cp ../LICENSE ./build/LICENSE 11 | if [ -e "../NOTICE" ]; then 12 | cp ../NOTICE ./build/NOTICE 13 | fi 14 | 15 | cat > build/README.md << EOF 16 | Website available [here](https://componego.github.io/). 17 | 18 | --- 19 | 20 | These files are auto-generated files from the [main repository](https://github.com/unpleasantcam/componego) 21 | so you don't have to commit changes directly to this repository. 22 | 23 | The [license](./LICENSE) of this repository matches the license of the parent repository. 24 | EOF 25 | 26 | cat > build/.gitignore << EOF 27 | .idea 28 | **/.idea 29 | .vs-code 30 | **/.vs-code 31 | EOF 32 | -------------------------------------------------------------------------------- /docs/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | componego-framework-docs: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | container_name: componego-framework-docs 9 | working_dir: /docs 10 | volumes: 11 | - .:/docs:cached 12 | ports: 13 | - "8123:8123" 14 | -------------------------------------------------------------------------------- /docs/mkdocs/hooks/navigation.py: -------------------------------------------------------------------------------- 1 | """ Adds additional logic to improve navigation. """ 2 | 3 | from mkdocs.plugins import event_priority 4 | from mkdocs.structure.pages import Page 5 | from mkdocs.config.defaults import MkDocsConfig 6 | from bs4 import BeautifulSoup 7 | 8 | 9 | @event_priority(50) 10 | def on_post_page(html: str, page: Page, config: MkDocsConfig) -> str: 11 | """ 12 | This hook changes the HTML tree by adding or removing some nodes or attributes. 13 | """ 14 | parsed_html = BeautifulSoup(html, 'html.parser') 15 | items = parsed_html.select('ul.md-nav__list .md-nav__item--section') 16 | # Menu items "Built-in Components" and "Examples". 17 | for index in [2, 3]: 18 | # The necessary checks for the keys in the list are missing 19 | # because we expect an exception if such a key does not exist. 20 | section = items[index] 21 | section['class'].remove('md-nav__item--section') 22 | section['class'] += ['toggle-color'] 23 | for toggle in section.select('.md-toggle--indeterminate'): 24 | toggle['class'].remove('md-toggle--indeterminate') 25 | for link in section.select('a'): 26 | if 'https://github.com' in link.get('href', ''): 27 | link['target'] = '_blank' 28 | for link in parsed_html.select('a[target="_blank"]'): 29 | if link.get('rel', None) is None: 30 | # noinspection SpellCheckingInspection 31 | link['rel'] = 'noopener' 32 | return str(parsed_html) 33 | -------------------------------------------------------------------------------- /docs/mkdocs/hooks/social.py: -------------------------------------------------------------------------------- 1 | """ Adds social params for each page. """ 2 | 3 | from mkdocs.plugins import event_priority 4 | from mkdocs.structure.pages import Page 5 | from mkdocs.config.defaults import MkDocsConfig 6 | 7 | 8 | @event_priority(50) 9 | def on_post_page(html: str, page: Page, config: MkDocsConfig) -> str: 10 | """ 11 | This hook adds social meta tags for each page. 12 | """ 13 | try: 14 | title = page.meta['social_meta']['title'] 15 | except KeyError: 16 | title = page.title 17 | try: 18 | description = page.meta['social_meta']['description'] 19 | except KeyError: 20 | description = config.site_description 21 | social_tags = config.theme.get_env().get_template('partials/meta_social_tags.html').render({ 22 | 'title': f'{config.theme["social_title_prefix"]} | {title}', 23 | 'description': description, 24 | 'url': page.canonical_url, 25 | 'image': f'{config["site_url"]}{config.theme["social_image"]}', 26 | }) 27 | return html.replace('', f'{social_tags}', 1) 28 | -------------------------------------------------------------------------------- /docs/mkdocs/theme/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unpleasantcam/componego/ca9559039e475ae00542874c1dc4fe462477df7d/docs/mkdocs/theme/assets/images/favicon.ico -------------------------------------------------------------------------------- /docs/mkdocs/theme/assets/images/gopher-eyes.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unpleasantcam/componego/ca9559039e475ae00542874c1dc4fe462477df7d/docs/mkdocs/theme/assets/images/gopher-eyes.webp -------------------------------------------------------------------------------- /docs/mkdocs/theme/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unpleasantcam/componego/ca9559039e475ae00542874c1dc4fe462477df7d/docs/mkdocs/theme/assets/images/logo.png -------------------------------------------------------------------------------- /docs/mkdocs/theme/assets/images/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unpleasantcam/componego/ca9559039e475ae00542874c1dc4fe462477df7d/docs/mkdocs/theme/assets/images/social.png -------------------------------------------------------------------------------- /docs/mkdocs/theme/assets/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | [data-md-color-primary=red] { 2 | --md-primary-fg-color: #dd2d58; 3 | } 4 | 5 | [data-md-color-scheme="default"] { 6 | --md-primary-bg-color: #fffdfa; 7 | --md-primary-bg-color--light: var(--md-primary-bg-color); 8 | background-color: var(--md-primary-bg-color); 9 | } 10 | 11 | html .md-footer-meta.md-typeset .site-generator { 12 | font-size: 9px; 13 | color: #44454b; 14 | } 15 | 16 | html .md-footer-meta.md-typeset .site-generator a { 17 | color: inherit; 18 | } 19 | 20 | body .md-content { 21 | counter-reset: h2; 22 | } 23 | 24 | .md-header__button.md-logo { 25 | margin: 0 0 0 0.2rem; 26 | padding: 0 0 0 0.4rem; 27 | } 28 | 29 | .md-header__button.md-logo > img { 30 | height: 1.7rem; 31 | } 32 | 33 | .md-progress { 34 | background-color: var(--md-accent-fg-color); 35 | height: 0.1rem; 36 | } 37 | 38 | .md-content .md-typeset a { 39 | color: var(--md-accent-fg-color); 40 | } 41 | 42 | .md-content .md-typeset a:hover { 43 | text-decoration: underline; 44 | } 45 | 46 | .md-content .md-typeset .componego-grid-cards { 47 | grid-gap: 0.4rem; 48 | display: grid; 49 | grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)); 50 | margin: 1em 0; 51 | } 52 | 53 | .md-content h2 { 54 | counter-reset: h3; 55 | } 56 | 57 | .md-content h2:before { 58 | counter-increment: h2; 59 | content: counter(h2) ". "; 60 | color: var(--md-primary-fg-color); 61 | font-weight: 700; 62 | } 63 | 64 | .md-content a.headerlink { 65 | text-decoration: none !important; 66 | color: var(--md-primary-fg-color) !important; 67 | } 68 | 69 | .md-content h3:before { 70 | counter-increment: h3; 71 | content: counter(h2) "." counter(h3) ". "; 72 | color: var(--md-primary-fg-color); 73 | font-weight: 700; 74 | } 75 | 76 | .md-typeset .componego-grid-cards .card { 77 | border: 0.05rem solid var(--md-primary-fg-color); 78 | border-radius: 0.1rem; 79 | display: block; 80 | margin: 0; 81 | padding: 0.8rem; 82 | font-size: 0.85rem; 83 | } 84 | 85 | .md-typeset .componego-grid-cards p { 86 | margin: 0; 87 | } 88 | 89 | .md-typeset .componego-grid-cards a { 90 | color: var(--md-primary-fg-color) !important; 91 | font-weight: 700; 92 | } 93 | 94 | .md-typeset .componego-grid-cards a:hover { 95 | text-decoration: none; 96 | } 97 | 98 | .md-typeset .componego-grid-cards .twemoji { 99 | margin-right: 10px; 100 | } 101 | 102 | .md-source .md-source__facts { 103 | display: none; 104 | } 105 | 106 | 107 | .md-content .twemoji.heart { 108 | color: var(--md-primary-fg-color); 109 | } 110 | 111 | .md-content ins { 112 | text-decoration: none; 113 | color: var(--md-primary-fg-color); 114 | } 115 | 116 | .md-content a.social-share-button { 117 | padding: .625em 1em; 118 | margin: 0.5em 0.5em 0.5em 0; 119 | } 120 | 121 | .md-content a.social-share-button:hover { 122 | text-decoration: none; 123 | } 124 | 125 | @media screen and (min-width:76.234375em) { 126 | .md-nav--primary .md-nav__item--nested.toggle-color > label { 127 | color: var(--md-default-fg-color--light); 128 | font-weight: 700; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /docs/mkdocs/theme/home.html: -------------------------------------------------------------------------------- 1 | {% extends "main.html" %} 2 | 3 | {% block tabs %} 4 | {{ super() }} 5 | 6 |
7 |
8 |
9 |
10 |

Componego Framework

11 |

{{ config.site_description }}

12 | Quick start 15 | Read the Docs 18 |
19 |
20 | {% for menu_item in config.extra.homepage_menu %} 21 | 24 | {% endfor %} 25 |
26 |
27 |
28 |
29 | 51 | {% endblock %} 52 | 53 | {% block content %}{% endblock %} 54 | -------------------------------------------------------------------------------- /docs/mkdocs/theme/partials/copyright.html: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /docs/mkdocs/theme/partials/meta_social_tags.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/pages/assets/images/warnings/goroutine-leak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unpleasantcam/componego/ca9559039e475ae00542874c1dc4fe462477df7d/docs/pages/assets/images/warnings/goroutine-leak.png -------------------------------------------------------------------------------- /docs/pages/contribution/guide.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | This section is intended for users who wish to contribute to the [framework](https://github.com/unpleasantcam/componego){:target="_blank"}. 4 | 5 |
6 | 7 | Contributions can include new features, changes to existing features, tests, documentation (such as developer guides, examples, or specifications), bug fixes, optimizations, or valuable suggestions. 8 | 9 | Please follow the main rules for making changes to the codebase: 10 | 11 | 1. We use [pre-commit](https://pre-commit.com/){:target="_blank"} to run a custom hook when code is committed. Please install it. 12 | 2. After making changes, please run the tests using our utility: 13 | ```text hl_lines="1" 14 | % make tests 15 | ``` 16 | 3. Please do not include third-party packages in the framework's codebase. 17 | 4. Before creating a pull request or merging to our repository, please perform a ^^git rebase^^. 18 | 5. The language of communication is English. All commits, comments, tasks, and questions must be written in English. 19 | 20 | You can use the following commands to quickly create a framework contributor environment: 21 | ```shell 22 | curl -sSL https://raw.githubusercontent.com/componego/componego/master/tools/create-contributor-env.sh | sh 23 | ``` 24 | or 25 | ```shell 26 | wget -O - https://raw.githubusercontent.com/componego/componego/master/tools/create-contributor-env.sh | sh 27 | ``` 28 | 29 |
30 | 31 | If you like our framework, you can share it with your friends, which will help the project develop further. 32 | 33 | [Twitter | X](https://twitter.com/share?url=github.com%2Fcomponego%2Fcomponego){:target="_blank" .md-button .social-share-button } 34 | [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=github.com%2Fcomponego%2Fcomponego){:target="_blank" .md-button .social-share-button } 35 | [Facebook](https://www.facebook.com/sharer/sharer.php?u=github.com%2Fcomponego%2Fcomponego){:target="_blank" .md-button .social-share-button } 36 |
37 | 38 | :octicons-heart-fill-24:{ .heart } Thank you. 39 | -------------------------------------------------------------------------------- /docs/pages/get-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | social_meta: 3 | title: Get Started 4 | --- 5 | Componego Framework seamlessly integrates components, dependency injection, configuration, and error handling, 6 | providing a robust foundation for building modular, scalable, and maintainable software systems. 7 | 8 | The framework embraces a component-based architecture. 9 | These components encapsulate specific functionalities and seamlessly integrate to construct more complex applications. 10 | By promoting code reusability and maintainability, the framework allows developers to work on independent units. 11 | 12 | Main features of the framework: 13 | 14 | 1. A well-organized application initialization process with a minimal main function. 15 | 2. Simple and powerful components. 16 | 3. Flexible dependency injection that doesn't require code generation. 17 | 4. The core of the framework does not depend on third-party packages. 18 | 5. Easily integrates with existing code and third-party packages. 19 | 6. No impact on the business logic of your application. You can write and organize it just as you did before. 20 | 7. Any entity of the framework can be replaced without changing previously written code. 21 | 8. Comprehensive and clear documentation for developers. 22 | 23 | The framework does not provide a database connection, web server, queue, and other features. 24 | Instead, we offer the ability to conveniently integrate these components for later reuse in your projects. 25 | You can use any packages you prefer; there are no limits. 26 | Any existing Golang package can be easily wrapped in the framework's components. 27 | 28 | !!! note 29 | The documentation provides a brief description of the main functions of the framework. 30 | You may find some aspects complicated, but we include links for each part described in another section. 31 | 32 | You need to create an application or component using this framework to fully understand it. 33 | Don't hesitate to open the [source code](https://github.com/unpleasantcam/componego){:target="_blank"}. 34 | There, you will find many interesting functions that are not described in the documentation. 35 | 36 |
37 |
38 |
39 | :octicons-star-fill-16:[How to create an application?](./impl/application.md) 40 |
41 |
42 | :octicons-play-16:[How to run an application?](./impl/runner.md) 43 |
44 |
45 | :octicons-database-16:[How to create a component?](./impl/component.md) 46 |
47 |
48 | :octicons-git-pull-request-16:[How to use dependency injections?](./impl/dependency.md) 49 |
50 |
51 | :octicons-plug-16:[How to use configuration?](./impl/config.md) 52 |
53 |
54 | :octicons-bug-16:[How to handle errors?](./impl/application.md#applicationerrorhandler) 55 |
56 |
57 | :octicons-copy-16:[How to create mocks?](./tests/mock.md) 58 |
59 |
60 | :octicons-terminal-16:[How to create tests?](./tests/runner.md) 61 |
62 |
63 |
64 | -------------------------------------------------------------------------------- /docs/pages/impl/driver.md: -------------------------------------------------------------------------------- 1 | # Application Driver 2 | 3 | ## Basic information 4 | 5 | The application driver plays a main role in Componego runtime by serving as the entry point that initializes 6 | and orchestrates all essential functions within an application. 7 | Essentially, it acts as the engine that kick-starts the execution of an application by coordinating various components, 8 | managing configurations, and initiating critical processes. 9 | This driver ensures a smooth and controlled stage setting for seamless operation. 10 | 11 | ## Differences between Runner 12 | 13 | The difference is minimal, but the driver can be shared among many applications, 14 | initiating the basic functions of the application based on the driver's options. 15 | Driver options control various aspects of the application, including the environment factory, dependency manager, 16 | configuration manager, input reader, output writer, and error output writer. 17 | These options are flexible and can be modified since they are only [options](./runner.md#specific-driver-options). 18 | 19 | In most cases, you don't need to be aware of the driver options. However, if you wish to modify any core aspects of the framework, 20 | you can explore the [source code](https://github.com/unpleasantcam/componego/tree/master/impl/driver){:target="_blank"} to see how it is implemented. 21 | 22 | ## Application initialization order 23 | 24 |
25 | ![Componego Flow](../assets/images/diagrams/componego-flow.svg){ width="622" height="1202" loading=lazy } 26 |
Componego Flow
27 |
28 | 29 | !!! note 30 | The red elements in the image can handle errors that occur in previous (or nested) functions. 31 | 32 | !!! note 33 | We recommend looking at this diagram again when you fully understand 34 | how to create [applications](./application.md) and [components](./application.md), and the entities they provide. 35 | 36 | The general order in which functions are called is as follows: 37 | 38 | 1. runner.Run 39 | 2. driver.RunApplication 40 | 3. application.ApplicationConfigInit 41 | 4. application.ApplicationComponents 42 | 5. component.ComponentComponents (+ getting components for each component) 43 | 6. component.ComponentDependencies (for each of the active components) 44 | 7. application.ApplicationDependencies 45 | 8. component.ComponentInit (for each of the active components) 46 | 9. application.ApplicationAction 47 | 10. component.ComponentStop (for each of the active components in reverse order) 48 | 11. application.ApplicationErrorHandler (If there was an error) 49 | 12. exit 50 | 51 | Not all methods are described here (if the [application](./application.md) or [component](./component.md) uses these methods). 52 | This list provides a sufficient overview of the application initialization order. 53 | 54 | !!! note 55 | The order of initialization and method calls is crucial when rewriting elements of the application. 56 | For example, an [application](./application.md) can rewrite [dependencies](./dependency.md) of [component](./component.md) 57 | because a method that returns dependencies for the application object (^^ApplicationDependencies^^) is called 58 | after the same function for components (^^ComponentDependencies^^). 59 | This behavior can be particularly useful when creating [mocks](../tests/mock.md). 60 | -------------------------------------------------------------------------------- /docs/pages/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | template: home.html 3 | title: Home 4 | social_meta: 5 | title: Homepage 6 | --- 7 | -------------------------------------------------------------------------------- /docs/pages/tests/runner.md: -------------------------------------------------------------------------------- 1 | # Tests Runner 2 | 3 | ## Basic Example 4 | 5 | This is the simplest way: 6 | ```go hl_lines="6 12-13" 7 | package tests 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/unpleasantcam/componego/tests/runner" 13 | 14 | "secret.com/project-x/tests/mocks" 15 | ) 16 | 17 | func TestExample(t *testing.T) { 18 | env, cancelEnv := runner.CreateTestEnvironment(t, mocks.NewApplicationMock(), nil) 19 | t.Cleanup(cancelEnv) 20 | // ... here you can use application environment. 21 | } 22 | ``` 23 | In the example above, we created the new [environment](../impl/environment.md) based on the [application mock](./mock.md). 24 | You can use this environment to run the necessary functions in your tests. 25 | 26 | The last argument of the function accepts the test options, including [driver options](../impl/runner.md#specific-driver-options), that will be applied to the current test. 27 | 28 | When the environment is canceled, all necessary functions will be called to stop the application. 29 | 30 | The framework is thread-safe so you can run tests in parallel. 31 | However, your personal code or the code of third-party libraries you use may not be thread-safe. 32 | 33 | !!! note 34 | In the example above, you can see how to test an application. 35 | 36 | To test [component](../impl/component.md), you must create an application that [depends](../impl/application.md#applicationcomponents) only on that component. 37 | For example, this way you can configure a component, because [this method](../impl/config.md) is in any application. 38 | 39 | ## Test Mode 40 | 41 | In tests, the application should be launched in ^^componego.TestMode^^. 42 | There are different [application launch modes](../impl/runner.md#application-mode). 43 | However, for tests, it is recommended to use ^^componego.TestMode^^. 44 | 45 | The code above runs the application in this mode, so please consider this detail in your implementation. 46 | -------------------------------------------------------------------------------- /docs/pages/warnings/goroutine-leak.md: -------------------------------------------------------------------------------- 1 | # Goroutine leak when exiting the application 2 | 3 | Only in development mode, you may see warnings when exiting the application that some goroutines are still running. 4 | 5 | This means that, in addition to the main goroutine, other goroutines are also running. 6 | In any case, all goroutines will be terminated after exiting the application. 7 | However, some goroutines may be performing important work that will be terminated forcefully and possibly unsuccessfully. 8 | 9 | Of course, you cannot guarantee that 100% of all goroutines will be completed, but you should strive for this. 10 | 11 | Some packages run goroutines that continuously monitor events. 12 | 13 | Here is an example of code and how you can detect a goroutine leak: 14 | ```go 15 | package main 16 | 17 | import ( 18 | "os" 19 | "os/signal" 20 | "runtime/pprof" 21 | "syscall" 22 | ) 23 | 24 | func main() { 25 | interruptChan := make(chan os.Signal, 1) 26 | signal.Notify(interruptChan, os.Interrupt, syscall.SIGTERM) 27 | // ... 28 | signal.Stop(interruptChan) 29 | // ... 30 | _ = pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) 31 | } 32 | ``` 33 | The output of this program will be approximately as follows: 34 | 35 | ![Goroutine Leak Debug](../assets/images/warnings/goroutine-leak.png) 36 | 37 | As you can see, we still have a goroutine that monitors signals. 38 | This goroutine is safe; however, there may be other goroutines present. 39 | 40 | Please ignore this warning if there are no unwanted goroutine leaks. 41 | -------------------------------------------------------------------------------- /docs/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "componego-documentation" 3 | version = "0.1.0" 4 | description = "Componego Documentation" 5 | authors = ["Volodymyr Konstanchuk"] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.10" 10 | mkdocs-material = "^9.5.36" 11 | mkdocs-minify-plugin = "^0.8.0" 12 | mkdocs-material-extensions = "^1.3.1" 13 | beautifulsoup4 = "^4.12.3" 14 | 15 | [build-system] 16 | requires = ["poetry-core"] 17 | build-backend = "poetry.core.masonry.api" 18 | -------------------------------------------------------------------------------- /environment.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package componego 18 | 19 | import ( 20 | "context" 21 | ) 22 | 23 | // Environment is an interface that describes the application environment. 24 | type Environment interface { 25 | // GetContext returns a current application context. 26 | GetContext() context.Context 27 | // SetContext sets a new application context. 28 | // The new context must inherit from the previous context. 29 | SetContext(ctx context.Context) error 30 | // Application returns a current application object. 31 | Application() Application 32 | // ApplicationIO returns an object for getting application input and output. 33 | ApplicationIO() ApplicationIO 34 | // ApplicationMode returns the mode in which the application is started. 35 | ApplicationMode() ApplicationMode 36 | // ConfigProvider returns an object for getting config. 37 | ConfigProvider() ConfigProvider 38 | // Components returns a sorted list of active application components. 39 | Components() []Component 40 | // DependencyInvoker returns an object to invoke dependencies. 41 | DependencyInvoker() DependencyInvoker 42 | } 43 | -------------------------------------------------------------------------------- /examples/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22-alpine 2 | 3 | WORKDIR /go/src/github.com/unpleasantcam/componego/examples 4 | 5 | CMD ["sleep", "infinity"] 6 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Please use the Docker files in this directory to run the application examples. 2 | -------------------------------------------------------------------------------- /examples/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | x-applications-volume: 4 | &applications-volume 5 | type: bind 6 | source: ../ 7 | target: /go/src/github.com/unpleasantcam/componego 8 | 9 | services: 10 | componego-example-hello-app: 11 | build: . 12 | volumes: 13 | - <<: *applications-volume 14 | entrypoint: [ "go", "run", "./hello-app/cmd/application/main.go" ] 15 | 16 | componego-example-url-shortener-app: 17 | build: . 18 | volumes: 19 | - <<: *applications-volume 20 | ports: 21 | - "8080:8080" 22 | environment: 23 | - URL_SHORTENER_PORT=8080 24 | - URL_SHORTENER_DB_USERNAME=secret_db_name 25 | - URL_SHORTENER_DB_PASSWORD=secret_db_password 26 | entrypoint: [ "sh", "-c", " 27 | set -eo pipefail; 28 | mkdir -p /apps/url-shortener/config; 29 | cp -f ./url-shortener-app/config/config.json.example /apps/url-shortener/config/production.config.json; 30 | go build -o /apps/url-shortener/app ./url-shortener-app/cmd/application/main.go; 31 | cd /apps/url-shortener/ && exec /apps/url-shortener/app; 32 | " ] 33 | -------------------------------------------------------------------------------- /examples/hello-app/cmd/application/dev/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/unpleasantcam/componego" 5 | "github.com/unpleasantcam/componego/impl/runner" 6 | 7 | "github.com/unpleasantcam/componego/examples/hello-app/internal/application" 8 | ) 9 | 10 | func main() { 11 | runner.RunAndExit(application.New(), componego.DeveloperMode) 12 | } 13 | -------------------------------------------------------------------------------- /examples/hello-app/cmd/application/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/unpleasantcam/componego" 5 | "github.com/unpleasantcam/componego/impl/runner" 6 | 7 | "github.com/unpleasantcam/componego/examples/hello-app/internal/application" 8 | ) 9 | 10 | func main() { 11 | runner.RunAndExit(application.New(), componego.ProductionMode) 12 | } 13 | -------------------------------------------------------------------------------- /examples/hello-app/internal/application/application.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/unpleasantcam/componego" 7 | "github.com/unpleasantcam/componego/impl/application" 8 | ) 9 | 10 | type Application struct{} 11 | 12 | func New() *Application { 13 | return &Application{} 14 | } 15 | 16 | // ApplicationName belongs to interface componego.Application. 17 | func (a *Application) ApplicationName() string { 18 | return "Hello World App v0.0.1" 19 | } 20 | 21 | // ApplicationAction belongs to interface componego.Application. 22 | func (a *Application) ApplicationAction(env componego.Environment, _ any) (int, error) { 23 | _, err := fmt.Fprintln(env.ApplicationIO().OutputWriter(), "Hello World!") 24 | return application.ExitWrapper(err) 25 | } 26 | 27 | var _ componego.Application = (*Application)(nil) 28 | -------------------------------------------------------------------------------- /examples/hello-app/tests/basic_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/unpleasantcam/componego" 9 | "github.com/unpleasantcam/componego/impl/application" 10 | "github.com/unpleasantcam/componego/impl/driver" 11 | "github.com/unpleasantcam/componego/tests/runner" 12 | 13 | "github.com/unpleasantcam/componego/examples/hello-app/tests/mocks" 14 | ) 15 | 16 | func TestBasic(t *testing.T) { 17 | buffer := &bytes.Buffer{} 18 | // We run tests inside mock of the current application example. 19 | // You can replace parts of the application specifically for the test in this application mock. 20 | env, cancelEnv := runner.CreateTestEnvironment(t, mocks.NewApplicationMock(), &runner.TestOptions{ 21 | Driver: driver.New(&driver.Options{ 22 | AppIO: application.NewIO(nil, buffer, buffer), 23 | }), 24 | }) 25 | t.Cleanup(cancelEnv) 26 | 27 | exitCode, err := env.Application().ApplicationAction(env, nil) 28 | if exitCode != componego.SuccessExitCode || err != nil { 29 | t.Fatal("the application stopped with an error") 30 | } 31 | if buffer.String() != fmt.Sprintln("Hello World!") { 32 | t.Fatal("different application output was expected") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/hello-app/tests/mocks/application.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "github.com/unpleasantcam/componego/examples/hello-app/internal/application" 5 | ) 6 | 7 | type ApplicationMock struct { 8 | *application.Application 9 | } 10 | 11 | func NewApplicationMock() *ApplicationMock { 12 | return &ApplicationMock{ 13 | Application: application.New(), 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/urfave-cli-integration/README.md: -------------------------------------------------------------------------------- 1 | An example of integrating the [Componego Framework](https://github.com/unpleasantcam/componego) with [Urfave CLI](https://github.com/urfave/cli) is available in [this repository](https://github.com/componego/urfave-cli-integration). 2 | -------------------------------------------------------------------------------- /examples/url-shortener-app/.gitignore: -------------------------------------------------------------------------------- 1 | config/* 2 | !config/*.example 3 | -------------------------------------------------------------------------------- /examples/url-shortener-app/cmd/application/dev/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/unpleasantcam/componego" 5 | "github.com/unpleasantcam/componego/impl/runner" 6 | "github.com/unpleasantcam/componego/libs/color" 7 | 8 | "github.com/unpleasantcam/componego/examples/url-shortener-app/internal/application" 9 | ) 10 | 11 | func main() { 12 | // This enables color text output. 13 | color.SetIsActive(true) 14 | // This is an entry point for launching the application in developer mode. 15 | runner.RunGracefullyAndExit(application.New(), componego.DeveloperMode) 16 | } 17 | -------------------------------------------------------------------------------- /examples/url-shortener-app/cmd/application/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/unpleasantcam/componego" 5 | "github.com/unpleasantcam/componego/impl/runner" 6 | 7 | "github.com/unpleasantcam/componego/examples/url-shortener-app/internal/application" 8 | ) 9 | 10 | func main() { 11 | // This is an entry point for launching the application in production mode. 12 | runner.RunGracefullyAndExit(application.New(), componego.ProductionMode) 13 | } 14 | -------------------------------------------------------------------------------- /examples/url-shortener-app/config/config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "addr": ":${ENV:URL_SHORTENER_PORT|8080}" 4 | }, 5 | "databases": { 6 | "main-storage": { 7 | "driver": "db-driver", 8 | "source": "${ENV:URL_SHORTENER_DB_USERNAME}:${ENV:URL_SHORTENER_DB_PASSWORD}@tcp(0.0.0.0:3306)/url-shortener" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/url-shortener-app/internal/application/application.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/unpleasantcam/componego" 7 | "github.com/unpleasantcam/componego/impl/application" 8 | "github.com/unpleasantcam/componego/impl/environment/managers/config" 9 | 10 | "github.com/unpleasantcam/componego/examples/url-shortener-app/internal/migration" 11 | "github.com/unpleasantcam/componego/examples/url-shortener-app/internal/repository" 12 | appServer "github.com/unpleasantcam/componego/examples/url-shortener-app/internal/server" 13 | "github.com/unpleasantcam/componego/examples/url-shortener-app/pkg/components/database" 14 | "github.com/unpleasantcam/componego/examples/url-shortener-app/pkg/components/server" 15 | 16 | "github.com/unpleasantcam/componego/examples/url-shortener-app/third_party/config-reader" 17 | _ "github.com/unpleasantcam/componego/examples/url-shortener-app/third_party/db-driver" 18 | ) 19 | 20 | type Application struct{} 21 | 22 | func New() *Application { 23 | return &Application{} 24 | } 25 | 26 | // ApplicationName belongs to interface componego.Application. 27 | func (a *Application) ApplicationName() string { 28 | return "Url Shortener App v0.0.1" 29 | } 30 | 31 | // ApplicationComponents belongs to interface componego.ApplicationComponents. 32 | func (a *Application) ApplicationComponents() ([]componego.Component, error) { 33 | return []componego.Component{ 34 | // This is the custom component implemented in this example 35 | // which provides access to the database using a standard database connection interface. 36 | database.NewComponent(), 37 | // This is an example of server component implementation. 38 | server.NewComponent(), 39 | }, nil 40 | } 41 | 42 | // ApplicationDependencies belongs to interface componego.ApplicationDependencies. 43 | func (a *Application) ApplicationDependencies() ([]componego.Dependency, error) { 44 | return []componego.Dependency{ 45 | // Pay attention to the implementation of the function to understand what dependencies it provides. 46 | repository.NewRedirectRepository, 47 | }, nil 48 | } 49 | 50 | // ApplicationConfigInit belongs to interface componego.ApplicationConfigInit. 51 | func (a *Application) ApplicationConfigInit(appMode componego.ApplicationMode, _ any) (settings map[string]any, err error) { 52 | switch appMode { 53 | case componego.ProductionMode: 54 | settings, err = config_reader.Read("./config/production.config.json") 55 | case componego.DeveloperMode: 56 | settings, err = config_reader.Read("./config/developer.config.json") 57 | case componego.TestMode: 58 | settings, err = config_reader.Read("./config/test.config.json") 59 | default: 60 | return nil, fmt.Errorf("not supported application mode: %d", appMode) 61 | } 62 | if err == nil { 63 | // If necessary, you can additionally process the settings after reading the configuration. 64 | // In this case, we add environment variables to the configuration. 65 | err = config.ProcessVariables(settings) 66 | } 67 | return settings, err 68 | } 69 | 70 | // ApplicationErrorHandler belongs to interface componego.ApplicationErrorHandler. 71 | func (a *Application) ApplicationErrorHandler(err error, _ componego.ApplicationIO, _ componego.ApplicationMode) error { 72 | // This method catches all previously unhandled errors. 73 | // You can process them in some way or return an error. 74 | // If you return an error, it will be handled at a lower level of the framework. 75 | return err 76 | } 77 | 78 | // ApplicationAction belongs to interface componego.Application. 79 | func (a *Application) ApplicationAction(env componego.Environment, _ any) (int, error) { 80 | // We always run migrations after the application is initialized. 81 | if _, err := env.DependencyInvoker().Invoke(migration.Run); err != nil { 82 | return application.ExitWrapper(err) 83 | } 84 | // Start server after the migrations are completed. 85 | _, err := env.DependencyInvoker().Invoke(appServer.Run) 86 | return application.ExitWrapper(err) 87 | } 88 | 89 | var ( 90 | _ componego.Application = (*Application)(nil) 91 | _ componego.ApplicationComponents = (*Application)(nil) 92 | _ componego.ApplicationDependencies = (*Application)(nil) 93 | _ componego.ApplicationConfigInit = (*Application)(nil) 94 | _ componego.ApplicationErrorHandler = (*Application)(nil) 95 | ) 96 | -------------------------------------------------------------------------------- /examples/url-shortener-app/internal/domain/domain.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type Redirect struct { 4 | Key string 5 | Url string 6 | } 7 | -------------------------------------------------------------------------------- /examples/url-shortener-app/internal/migration/migration.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "github.com/unpleasantcam/componego/examples/url-shortener-app/pkg/components/database" 5 | ) 6 | 7 | const sql = ` 8 | CREATE TABLE IF NOT EXISTS redirects ( 9 | entity_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, 10 | url_key VARCHAR(10) NOT NULL, 11 | url VARCHAR(255) NOT NULL, 12 | PRIMARY KEY(entity_id), 13 | UNIQUE KEY message_key(url_key) 14 | ) ENGINE = INNODB DEFAULT CHARSET = utf8mb3 COLLATE = utf8mb3_general_ci COMMENT = 'Table with redirects' 15 | ` 16 | 17 | func Run(dbProvider database.Provider) error { 18 | // Note that dependency 'dbProvider' is present in the application 19 | // because we added a component to the application that provides that dependency. 20 | db, err := dbProvider.GetConnection("main-storage") 21 | if err == nil { 22 | _, err = db.Exec(sql) 23 | } 24 | return err 25 | } 26 | -------------------------------------------------------------------------------- /examples/url-shortener-app/internal/repository/repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | 8 | "github.com/unpleasantcam/componego/examples/url-shortener-app/internal/domain" 9 | "github.com/unpleasantcam/componego/examples/url-shortener-app/internal/utils" 10 | "github.com/unpleasantcam/componego/examples/url-shortener-app/pkg/components/database" 11 | ) 12 | 13 | type RedirectRepository interface { 14 | Add(ctx context.Context, url string) (*domain.Redirect, error) 15 | Get(ctx context.Context, key string) (*domain.Redirect, error) 16 | } 17 | 18 | type redirectRepository struct { 19 | db *sql.DB 20 | } 21 | 22 | func NewRedirectRepository(dbProvider database.Provider) (RedirectRepository, error) { 23 | db, err := dbProvider.GetConnection("main-storage") 24 | if err != nil { 25 | return nil, err 26 | } 27 | return &redirectRepository{ 28 | db: db, 29 | }, nil 30 | } 31 | 32 | func (r *redirectRepository) Add(ctx context.Context, url string) (*domain.Redirect, error) { 33 | randomString := utils.GetRandomString(10) 34 | if randomString == "" { 35 | return nil, errors.New("could not get the new url key") 36 | } 37 | stmt, err := r.db.PrepareContext(ctx, `INSERT INTO redirects(url_key, url) VALUES(?, ?)`) 38 | if err != nil { 39 | return nil, err 40 | } 41 | defer func() { 42 | _ = stmt.Close() 43 | }() 44 | redirect := &domain.Redirect{ 45 | Key: randomString, 46 | Url: url, 47 | } 48 | if _, err = stmt.ExecContext(ctx, redirect.Key, redirect.Url); err != nil { 49 | return nil, err 50 | } 51 | return redirect, nil 52 | } 53 | 54 | func (r *redirectRepository) Get(ctx context.Context, key string) (*domain.Redirect, error) { 55 | stmt, err := r.db.PrepareContext(ctx, `SELECT url FROM redirects WHERE url_key = ?`) 56 | if err != nil { 57 | return nil, err 58 | } 59 | defer func() { 60 | _ = stmt.Close() 61 | }() 62 | redirect := &domain.Redirect{ 63 | Key: key, 64 | } 65 | if err = stmt.QueryRowContext(ctx, key).Scan(&redirect.Url); err != nil { 66 | return nil, err 67 | } 68 | return redirect, nil 69 | } 70 | -------------------------------------------------------------------------------- /examples/url-shortener-app/internal/server/handlers/index.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | func NewIndexGetHandler() http.HandlerFunc { 9 | return func(response http.ResponseWriter, request *http.Request) { 10 | _, _ = fmt.Fprint(response, "It works") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/url-shortener-app/internal/server/handlers/redirect.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/unpleasantcam/componego/examples/url-shortener-app/internal/repository" 9 | "github.com/unpleasantcam/componego/examples/url-shortener-app/internal/server/json" 10 | "github.com/unpleasantcam/componego/examples/url-shortener-app/internal/utils" 11 | ) 12 | 13 | // -------------------- GET Redirect ------------------- 14 | 15 | func NewRedirectGetHandler(redirectRepository repository.RedirectRepository) http.HandlerFunc { 16 | return func(w http.ResponseWriter, r *http.Request) { 17 | key := r.PathValue("key") 18 | if redirect, err := redirectRepository.Get(r.Context(), key); err == nil { 19 | http.Redirect(w, r, redirect.Url, http.StatusPermanentRedirect) 20 | return 21 | } 22 | http.NotFound(w, r) 23 | } 24 | } 25 | 26 | // -------------------- PUT Redirect ------------------- 27 | 28 | func NewRedirectPutHandler(redirectRepository repository.RedirectRepository) http.HandlerFunc { 29 | return func(w http.ResponseWriter, r *http.Request) { 30 | type request struct { 31 | Url string `json:"url"` 32 | } 33 | 34 | type response struct { 35 | NewUrl string `json:"newUrl"` 36 | } 37 | 38 | requestData, err := json.Get[request](r) 39 | if err != nil { 40 | json.Send(w, nil, errors.New("error decoding JSON")) 41 | return 42 | } 43 | if !utils.IsValidUrl(requestData.Url) { 44 | json.Send(w, nil, errors.New("invalid url")) 45 | return 46 | } 47 | 48 | if redirect, err := redirectRepository.Add(r.Context(), requestData.Url); err != nil { 49 | json.Send(w, nil, errors.New("failed to add redirect")) 50 | } else if r.TLS == nil { 51 | // noinspection ALL 52 | json.Send(w, &response{ 53 | NewUrl: fmt.Sprintf("http://%s/get/%s", r.Host, redirect.Key), 54 | }, nil) 55 | } else { 56 | json.Send(w, &response{ 57 | NewUrl: fmt.Sprintf("https://%s/get/%s", r.Host, redirect.Key), 58 | }, nil) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/url-shortener-app/internal/server/json/json.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | type jsonResponse struct { 9 | Status bool `json:"status"` 10 | Error string `json:"error,omitempty"` 11 | Data any `json:"data,omitempty"` 12 | } 13 | 14 | func Get[T any](request *http.Request) (value T, err error) { 15 | err = json.NewDecoder(request.Body).Decode(&value) 16 | return value, err 17 | } 18 | 19 | func Send(response http.ResponseWriter, data any, err error) { 20 | jsonResponse := &jsonResponse{ 21 | Status: err == nil, 22 | Data: data, 23 | } 24 | if !jsonResponse.Status { 25 | jsonResponse.Error = err.Error() 26 | } 27 | response.Header().Set("Content-Type", "application/json") 28 | if err = json.NewEncoder(response).Encode(jsonResponse); err != nil { 29 | http.Error(response, "error sending response", http.StatusBadRequest) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/url-shortener-app/internal/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/unpleasantcam/componego" 7 | 8 | "github.com/unpleasantcam/componego/examples/url-shortener-app/internal/server/handlers" 9 | "github.com/unpleasantcam/componego/examples/url-shortener-app/pkg/components/server" 10 | ) 11 | 12 | func Run(env componego.Environment, s *server.Server) error { 13 | if err := errors.Join( 14 | s.AddRouter("GET /", handlers.NewIndexGetHandler), 15 | s.AddRouter("PUT /create", handlers.NewRedirectPutHandler), 16 | s.AddRouter("GET /get/{key}", handlers.NewRedirectGetHandler), 17 | ); err != nil { 18 | return err 19 | } 20 | return s.Run(env.GetContext()) 21 | } 22 | -------------------------------------------------------------------------------- /examples/url-shortener-app/internal/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/rand" 5 | "math/big" 6 | "net/url" 7 | ) 8 | 9 | // noinspection SpellCheckingInspection 10 | const ( 11 | randomCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 12 | ) 13 | 14 | func GetRandomString(length int) string { 15 | maxNumber := big.NewInt(int64(len(randomCharset))) 16 | result := make([]byte, length) 17 | for i := 0; i < length; i++ { 18 | randomValue, err := rand.Int(rand.Reader, maxNumber) 19 | if err != nil { 20 | return "" 21 | } 22 | result[i] = randomCharset[randomValue.Int64()] 23 | } 24 | return string(result) 25 | } 26 | 27 | func IsValidUrl(value string) bool { 28 | if _, err := url.ParseRequestURI(value); err != nil { 29 | return false 30 | } 31 | parsedUrl, err := url.Parse(value) 32 | if err != nil || parsedUrl.Scheme == "" { 33 | return false 34 | } 35 | return true 36 | } 37 | -------------------------------------------------------------------------------- /examples/url-shortener-app/pkg/components/database/component.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "github.com/unpleasantcam/componego" 5 | 6 | "github.com/unpleasantcam/componego/examples/url-shortener-app/pkg/components/database/internal" 7 | ) 8 | 9 | type ( 10 | Provider = internal.Provider 11 | ) 12 | 13 | type Component struct{} 14 | 15 | func NewComponent() *Component { 16 | return &Component{} 17 | } 18 | 19 | // ComponentIdentifier belongs to interface componego.Component. 20 | func (c *Component) ComponentIdentifier() string { 21 | return "componego:examples:database" 22 | } 23 | 24 | // ComponentVersion belongs to interface componego.Component. 25 | func (c *Component) ComponentVersion() string { 26 | return "0.0.1" 27 | } 28 | 29 | // ComponentDependencies belongs to interface componego.ComponentDependencies. 30 | func (c *Component) ComponentDependencies() ([]componego.Dependency, error) { 31 | return []componego.Dependency{ 32 | internal.NewProvider, 33 | }, nil 34 | } 35 | 36 | var ( 37 | _ componego.Component = (*Component)(nil) 38 | _ componego.ComponentDependencies = (*Component)(nil) 39 | ) 40 | -------------------------------------------------------------------------------- /examples/url-shortener-app/pkg/components/database/examples/config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "databases": { 3 | "": { 4 | "driver": "", 5 | "source": "" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/url-shortener-app/pkg/components/database/internal/config.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/unpleasantcam/componego" 7 | "github.com/unpleasantcam/componego/impl/environment/managers/config" 8 | "github.com/unpleasantcam/componego/impl/processors" 9 | ) 10 | 11 | func getDataSourceName(connectionName string, env componego.Environment) (string, error) { 12 | return config.Get[string](fmt.Sprintf("databases.%s.source", connectionName), processors.Multi( 13 | processors.IsRequired(), 14 | processors.ToString(), 15 | ), env) 16 | } 17 | 18 | func getDriver(connectionName string, env componego.Environment) (string, error) { 19 | return config.Get[string](fmt.Sprintf("databases.%s.driver", connectionName), processors.Multi( 20 | processors.IsRequired(), 21 | processors.ToString(), 22 | ), env) 23 | } 24 | -------------------------------------------------------------------------------- /examples/url-shortener-app/pkg/components/database/internal/provider.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "sync" 9 | 10 | "github.com/unpleasantcam/componego" 11 | ) 12 | 13 | type Provider interface { 14 | GetConnection(name string) (*sql.DB, error) 15 | CreateConnection(name string) (*sql.DB, error) 16 | CloseConnection(name string) error 17 | } 18 | 19 | type provider struct { 20 | mutex sync.Mutex 21 | env componego.Environment 22 | list map[string]*sql.DB 23 | } 24 | 25 | func NewProvider(env componego.Environment) Provider { 26 | dbProvider := &provider{ 27 | mutex: sync.Mutex{}, 28 | env: env, 29 | list: make(map[string]*sql.DB, 2), 30 | } 31 | return dbProvider 32 | } 33 | 34 | func (p *provider) GetConnection(name string) (*sql.DB, error) { 35 | p.mutex.Lock() 36 | defer p.mutex.Unlock() 37 | if p.list == nil { 38 | // The application is stopping now. 39 | // The function to close all provider was called. 40 | return nil, fmt.Errorf("you cannot create a connection to '%s'. Make sure the order of the components is correct", name) 41 | } else if connection, ok := p.list[name]; ok { 42 | return connection, nil 43 | } else if connection, err := p.CreateConnection(name); err == nil { 44 | p.list[name] = connection 45 | return connection, nil 46 | } else { //nolint:revive 47 | return nil, err 48 | } 49 | } 50 | 51 | func (p *provider) CreateConnection(name string) (db *sql.DB, err error) { 52 | var driver, source string 53 | if driver, err = getDriver(name, p.env); err != nil { 54 | return nil, err 55 | } 56 | if source, err = getDataSourceName(name, p.env); err != nil { 57 | return nil, err 58 | } 59 | db, err = sql.Open(driver, source) 60 | if err != nil { 61 | return nil, err 62 | } else if err = db.PingContext(p.env.GetContext()); err != nil { 63 | return nil, err 64 | } 65 | return db, nil 66 | } 67 | 68 | func (p *provider) CloseConnection(name string) error { 69 | p.mutex.Lock() 70 | defer p.mutex.Unlock() 71 | if connection, ok := p.list[name]; ok { 72 | delete(p.list, name) 73 | return connection.Close() 74 | } 75 | return fmt.Errorf("not found connection with name '%s'", name) 76 | } 77 | 78 | func (p *provider) Close() (err error) { 79 | p.mutex.Lock() 80 | defer p.mutex.Unlock() 81 | errs := make([]error, 0, len(p.list)) 82 | defer func() { 83 | err = errors.Join(errs...) 84 | }() 85 | for _, connection := range p.list { 86 | // We use deferred functions to ensure that all connections are closed even if a panic occurs. 87 | // Panic will be intercepted at the framework core level. 88 | // noinspection ALL 89 | defer func(connection io.Closer) { 90 | errs = append(errs, connection.Close()) 91 | }(connection) 92 | } 93 | // It sets a flag that the connection can no longer be opened. 94 | p.list = nil 95 | return err 96 | } 97 | 98 | var ( 99 | _ Provider = (*provider)(nil) 100 | _ io.Closer = (*provider)(nil) 101 | ) 102 | -------------------------------------------------------------------------------- /examples/url-shortener-app/pkg/components/database/tests/database_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/unpleasantcam/componego/tests/runner" 7 | 8 | "github.com/unpleasantcam/componego/examples/url-shortener-app/pkg/components/database" 9 | "github.com/unpleasantcam/componego/examples/url-shortener-app/pkg/components/database/tests/mocks" 10 | ) 11 | 12 | func TestComponent(t *testing.T) { 13 | env, cancelEnv := runner.CreateTestEnvironment(t, mocks.NewApplicationMock(), nil) 14 | t.Cleanup(cancelEnv) 15 | t.Run("basic", func(t *testing.T) { 16 | t.Parallel() 17 | _, err := env.DependencyInvoker().Invoke(func(dbProvider database.Provider) error { 18 | db, err := dbProvider.GetConnection("test-storage") 19 | if err != nil { 20 | return err 21 | } 22 | var version string 23 | if err = db.QueryRow(`SELECT VERSION()`).Scan(&version); err != nil { 24 | return err 25 | } 26 | if version != "0.0.1" { 27 | t.Fatal("no expected data") 28 | } 29 | return nil 30 | }) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /examples/url-shortener-app/pkg/components/database/tests/mocks/application.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "github.com/unpleasantcam/componego" 5 | "github.com/unpleasantcam/componego/impl/application" 6 | 7 | "github.com/unpleasantcam/componego/examples/url-shortener-app/pkg/components/database" 8 | 9 | _ "github.com/unpleasantcam/componego/examples/url-shortener-app/third_party/db-driver" 10 | ) 11 | 12 | func NewApplicationMock() componego.Application { 13 | factory := application.NewFactory("Application for Test Database Component") 14 | factory.SetApplicationComponents(func() ([]componego.Component, error) { 15 | return []componego.Component{ 16 | database.NewComponent(), 17 | }, nil 18 | }) 19 | factory.SetApplicationConfigInit(func(_ componego.ApplicationMode, _ any) (map[string]any, error) { 20 | return map[string]any{ 21 | "databases.test-storage.driver": "db-driver-mock", 22 | "databases.test-storage.source": "...", 23 | }, nil 24 | }) 25 | return factory.Build() 26 | } 27 | -------------------------------------------------------------------------------- /examples/url-shortener-app/pkg/components/server/component.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/unpleasantcam/componego" 5 | 6 | "github.com/unpleasantcam/componego/examples/url-shortener-app/pkg/components/server/internal" 7 | ) 8 | 9 | type ( 10 | Server = internal.Server 11 | ) 12 | 13 | type Component struct{} 14 | 15 | func NewComponent() *Component { 16 | return &Component{} 17 | } 18 | 19 | // ComponentIdentifier belongs to interface componego.Component. 20 | func (c *Component) ComponentIdentifier() string { 21 | return "componego:examples:server" 22 | } 23 | 24 | // ComponentVersion belongs to interface componego.Component. 25 | func (c *Component) ComponentVersion() string { 26 | return "0.0.1" 27 | } 28 | 29 | // ComponentDependencies belongs to interface componego.ComponentDependencies. 30 | func (c *Component) ComponentDependencies() ([]componego.Dependency, error) { 31 | return []componego.Dependency{ 32 | internal.NewServer, 33 | internal.NewConfig, 34 | }, nil 35 | } 36 | 37 | var ( 38 | _ componego.Component = (*Component)(nil) 39 | _ componego.ComponentDependencies = (*Component)(nil) 40 | ) 41 | -------------------------------------------------------------------------------- /examples/url-shortener-app/pkg/components/server/examples/config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "addr": "" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/url-shortener-app/pkg/components/server/internal/config.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/unpleasantcam/componego" 7 | "github.com/unpleasantcam/componego/impl/environment/managers/config" 8 | "github.com/unpleasantcam/componego/impl/processors" 9 | ) 10 | 11 | type Config struct { 12 | Addr string 13 | ReadTimeout time.Duration 14 | ReadHeaderTimeout time.Duration 15 | WriteTimeout time.Duration 16 | IdleTimeout time.Duration 17 | StopTimeout time.Duration 18 | } 19 | 20 | func NewConfig(env componego.Environment) *Config { 21 | return &Config{ 22 | Addr: config.GetOrPanic[string]("server.addr", processors.Multi( 23 | processors.DefaultValue(":3030"), 24 | processors.ToString(), 25 | ), env), 26 | ReadTimeout: 1 * time.Second, 27 | ReadHeaderTimeout: 1 * time.Second, 28 | WriteTimeout: 1 * time.Second, 29 | IdleTimeout: 30 * time.Second, 30 | StopTimeout: 10 * time.Second, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/url-shortener-app/pkg/components/server/internal/server.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | 9 | "github.com/unpleasantcam/componego" 10 | "github.com/unpleasantcam/componego/impl/environment/managers/dependency" 11 | 12 | "github.com/unpleasantcam/componego/examples/url-shortener-app/third_party/errgroup" 13 | "github.com/unpleasantcam/componego/examples/url-shortener-app/third_party/servermux" 14 | ) 15 | 16 | type Server struct { 17 | env componego.Environment 18 | config *Config 19 | router http.Handler 20 | addRouter servermux.AddHandler 21 | } 22 | 23 | func NewServer(env componego.Environment, config *Config) *Server { 24 | router, addRouter := servermux.CreateRouter() 25 | return &Server{ 26 | env: env, 27 | config: config, 28 | router: router, 29 | addRouter: addRouter, 30 | } 31 | } 32 | 33 | func (s *Server) AddRouter(pattern string, handler any) error { 34 | castedHandler, err := dependency.Invoke[http.HandlerFunc](handler, s.env) 35 | if err != nil { 36 | return err 37 | } 38 | s.addRouter(pattern, castedHandler) 39 | return nil 40 | } 41 | 42 | func (s *Server) Run(ctx context.Context) error { 43 | if err := s.debug("server is starting on %s...\n", s.config.Addr); err != nil { 44 | return err 45 | } 46 | server := &http.Server{ 47 | Addr: s.config.Addr, 48 | ReadTimeout: s.config.ReadTimeout, 49 | ReadHeaderTimeout: s.config.ReadHeaderTimeout, 50 | WriteTimeout: s.config.WriteTimeout, 51 | IdleTimeout: s.config.IdleTimeout, 52 | Handler: s.router, 53 | } 54 | stopChan := make(chan struct{}, 1) 55 | errGroup := errgroup.Group{} 56 | errGroup.Go(func() error { 57 | defer func() { 58 | stopChan <- struct{}{} 59 | }() 60 | if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { 61 | return err 62 | } 63 | return nil 64 | }) 65 | errGroup.Go(func() error { 66 | select { 67 | case <-ctx.Done(): 68 | case <-stopChan: 69 | return nil 70 | } 71 | err := s.debug("trying to stop the server on %s...\n", s.config.Addr) 72 | cancelableCtx, cancelCtx := context.WithTimeout(context.Background(), s.config.StopTimeout) 73 | defer cancelCtx() 74 | return errors.Join(server.Shutdown(cancelableCtx), err) 75 | }) 76 | return errGroup.Wait() 77 | } 78 | 79 | func (s *Server) debug(format string, args ...any) error { 80 | _, err := fmt.Fprintf(s.env.ApplicationIO().OutputWriter(), format, args...) 81 | return err 82 | } 83 | -------------------------------------------------------------------------------- /examples/url-shortener-app/pkg/components/test-server/component.go: -------------------------------------------------------------------------------- 1 | package test_server 2 | 3 | import ( 4 | "github.com/unpleasantcam/componego" 5 | 6 | "github.com/unpleasantcam/componego/examples/url-shortener-app/pkg/components/server" 7 | "github.com/unpleasantcam/componego/examples/url-shortener-app/pkg/components/test-server/internal" 8 | ) 9 | 10 | type ( 11 | TestServer = internal.TestServer 12 | ) 13 | 14 | type Component struct{} 15 | 16 | func NewComponent() *Component { 17 | return &Component{} 18 | } 19 | 20 | // ComponentIdentifier belongs to interface componego.Component. 21 | func (c *Component) ComponentIdentifier() string { 22 | return "componego:examples:test-server" 23 | } 24 | 25 | // ComponentVersion belongs to interface componego.Component. 26 | func (c *Component) ComponentVersion() string { 27 | return "0.0.1" 28 | } 29 | 30 | // ComponentComponents belongs to interface componego.ComponentComponents. 31 | func (c *Component) ComponentComponents() ([]componego.Component, error) { 32 | return []componego.Component{ 33 | server.NewComponent(), 34 | }, nil 35 | } 36 | 37 | // ComponentDependencies belongs to interface componego.ComponentDependencies. 38 | func (c *Component) ComponentDependencies() ([]componego.Dependency, error) { 39 | return []componego.Dependency{ 40 | internal.NewServer, 41 | }, nil 42 | } 43 | 44 | var ( 45 | _ componego.Component = (*Component)(nil) 46 | _ componego.ComponentComponents = (*Component)(nil) 47 | _ componego.ComponentDependencies = (*Component)(nil) 48 | ) 49 | -------------------------------------------------------------------------------- /examples/url-shortener-app/pkg/components/test-server/internal/server.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | 7 | "github.com/unpleasantcam/componego" 8 | "github.com/unpleasantcam/componego/impl/environment/managers/dependency" 9 | 10 | "github.com/unpleasantcam/componego/examples/url-shortener-app/third_party/servermux" 11 | ) 12 | 13 | type TestServer struct { 14 | env componego.Environment 15 | } 16 | 17 | func NewServer(env componego.Environment) *TestServer { 18 | return &TestServer{ 19 | env: env, 20 | } 21 | } 22 | 23 | func (s *TestServer) Run( 24 | configureCallback func(addRouter func(pattern string, handler any)), 25 | runCallback func(baseUrl string), 26 | ) { 27 | mainRouter, addRouter := servermux.CreateRouter() 28 | configureCallback(func(pattern string, handler any) { 29 | castedHandler, err := dependency.Invoke[http.HandlerFunc](handler, s.env) 30 | if err != nil { 31 | panic(err) 32 | } 33 | addRouter(pattern, castedHandler) 34 | }) 35 | serverInstance := httptest.NewServer(mainRouter) 36 | defer serverInstance.Close() 37 | runCallback(serverInstance.URL) 38 | } 39 | -------------------------------------------------------------------------------- /examples/url-shortener-app/tests/integration_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "testing" 9 | 10 | "github.com/unpleasantcam/componego/tests/runner" 11 | 12 | "github.com/unpleasantcam/componego/examples/url-shortener-app/internal/server/handlers" 13 | "github.com/unpleasantcam/componego/examples/url-shortener-app/internal/utils" 14 | "github.com/unpleasantcam/componego/examples/url-shortener-app/pkg/components/test-server" 15 | "github.com/unpleasantcam/componego/examples/url-shortener-app/tests/mocks" 16 | ) 17 | 18 | func TestIntegration(t *testing.T) { 19 | // We run tests inside mock of the current application example. 20 | // You can replace parts of the application specifically for the test in this application mock. 21 | env, cancelEnv := runner.CreateTestEnvironment(t, mocks.NewApplicationMock(), nil) 22 | t.Cleanup(cancelEnv) 23 | t.Run("create urls", func(t *testing.T) { 24 | t.Parallel() // Parallel running of tests is supported. 25 | _, err := env.DependencyInvoker().Invoke(func(s *test_server.TestServer) { 26 | s.Run( 27 | func(addRouter func(pattern string, handler any)) { 28 | addRouter("PUT /create", handlers.NewRedirectPutHandler) 29 | addRouter("GET /get/{key}", handlers.NewRedirectGetHandler) 30 | }, 31 | func(baseUrl string) { 32 | longUrl := fmt.Sprintf("https://%s.com/", utils.GetRandomString(100)) 33 | shortUrl := getShortUrl(t, baseUrl+"/create", longUrl) 34 | if getLongUrl(t, shortUrl) != longUrl { 35 | t.Fatal("short and long urls do not match") 36 | } 37 | }, 38 | ) 39 | }) 40 | if err != nil { 41 | t.Fatalf("send request error: %s", err) 42 | } 43 | }) 44 | } 45 | 46 | func getShortUrl(t *testing.T, endpoint string, longUrl string) string { 47 | response, err := (&http.Client{}).Do((func(body string) *http.Request { 48 | request, err := http.NewRequest(http.MethodPut, endpoint, bytes.NewBuffer([]byte(body))) 49 | if err != nil { 50 | t.Fatalf("send request error: %s", err) 51 | } 52 | request.Header.Set("Content-Type", "application/json") 53 | return request 54 | })(fmt.Sprintf(`{ "url": "%s" }`, longUrl))) 55 | if err != nil { 56 | t.Fatalf("send request error: %s", err) 57 | } 58 | defer func() { 59 | _ = response.Body.Close() 60 | }() 61 | if response.StatusCode != http.StatusOK { 62 | t.Fatalf("invalid response status: %d", response.StatusCode) 63 | } 64 | var responseAsStruct struct { 65 | Status bool `json:"status"` 66 | Error string `json:"error,omitempty"` 67 | Data struct { 68 | NewUrl string `json:"newUrl"` 69 | } `json:"data,omitempty"` 70 | } 71 | if err = json.NewDecoder(response.Body).Decode(&responseAsStruct); err != nil { 72 | t.Fatal("invalid response received") 73 | } else if responseAsStruct.Status != true { 74 | t.Fatal("redirect was not created") 75 | } 76 | return responseAsStruct.Data.NewUrl 77 | } 78 | 79 | func getLongUrl(t *testing.T, endpoint string) string { 80 | response, err := (&http.Client{ 81 | CheckRedirect: func(_ *http.Request, _ []*http.Request) error { 82 | return http.ErrUseLastResponse 83 | }, 84 | }).Get(endpoint) 85 | if err != nil { 86 | t.Fatalf("send request error: %s", err) 87 | } 88 | defer func() { 89 | _ = response.Body.Close() 90 | }() 91 | if response.StatusCode != http.StatusPermanentRedirect { 92 | t.Fatal("failed to get redirect") 93 | } 94 | return response.Header.Get("Location") 95 | } 96 | -------------------------------------------------------------------------------- /examples/url-shortener-app/tests/mocks/application.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "github.com/unpleasantcam/componego" 5 | 6 | "github.com/unpleasantcam/componego/examples/url-shortener-app/internal/application" 7 | "github.com/unpleasantcam/componego/examples/url-shortener-app/pkg/components/test-server" 8 | ) 9 | 10 | type ApplicationMock struct { 11 | *application.Application 12 | } 13 | 14 | func NewApplicationMock() *ApplicationMock { 15 | return &ApplicationMock{ 16 | Application: application.New(), 17 | } 18 | } 19 | 20 | // ApplicationComponents belongs to interface componego.ApplicationComponents. 21 | func (a *ApplicationMock) ApplicationComponents() ([]componego.Component, error) { 22 | components, err := a.Application.ApplicationComponents() 23 | // Add the component that provides access to test server dependency. 24 | components = append(components, test_server.NewComponent()) 25 | return components, err 26 | } 27 | 28 | // ApplicationConfigInit belongs to interface componego.ApplicationConfigInit. 29 | func (a *ApplicationMock) ApplicationConfigInit(_ componego.ApplicationMode, _ any) (map[string]any, error) { 30 | return map[string]any{ 31 | "databases.main-storage.driver": "db-driver-mock", 32 | "databases.main-storage.source": "...", 33 | }, nil 34 | } 35 | 36 | var ( 37 | _ componego.Application = (*ApplicationMock)(nil) 38 | _ componego.ApplicationComponents = (*ApplicationMock)(nil) 39 | _ componego.ApplicationDependencies = (*ApplicationMock)(nil) 40 | _ componego.ApplicationConfigInit = (*ApplicationMock)(nil) 41 | _ componego.ApplicationErrorHandler = (*ApplicationMock)(nil) 42 | ) 43 | -------------------------------------------------------------------------------- /examples/url-shortener-app/third_party/Readme.md: -------------------------------------------------------------------------------- 1 | This directory contains very simple libraries. 2 | 3 | [Componego Framework](https://github.com/unpleasantcam/componego) does NOT depend on other packages, 4 | but we would like to provide examples of how you can use our framework with your favorite libraries. 5 | For this purpose, we created a few libraries and placed them in this directory. 6 | -------------------------------------------------------------------------------- /examples/url-shortener-app/third_party/config-reader/reader.go: -------------------------------------------------------------------------------- 1 | package config_reader 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | func Read(filename string) (settings map[string]any, err error) { 10 | filename = filepath.Clean(filename) 11 | // We get the full file name for a nice error in case of a file reading error. 12 | if filename, err = filepath.Abs(filename); err != nil { 13 | return nil, err 14 | } 15 | if data, err := os.ReadFile(filename); err != nil { 16 | return nil, err 17 | } else if err = json.Unmarshal(data, &settings); err != nil { 18 | return nil, err 19 | } 20 | return settings, err 21 | } 22 | -------------------------------------------------------------------------------- /examples/url-shortener-app/third_party/db-driver/queries.go: -------------------------------------------------------------------------------- 1 | package db_driver 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | "strings" 7 | ) 8 | 9 | // List of hardcoded database queries because the driver in this example does not provide a real connection to the database. 10 | var queries = []struct { 11 | query string 12 | matcher func(actualQuery string, expectedQuery string) bool 13 | handler func(ctx context.Context, conn *connection, args []driver.NamedValue) (driver.Rows, error) 14 | }{ 15 | { 16 | query: `CREATE TABLE IF NOT EXISTS redirects`, 17 | matcher: func(actualQuery string, expectedQuery string) bool { 18 | return strings.Contains(actualQuery, expectedQuery) 19 | }, 20 | handler: func(_ context.Context, _ *connection, _ []driver.NamedValue) (driver.Rows, error) { 21 | return nil, nil 22 | }, 23 | }, 24 | { 25 | query: `INSERT INTO redirects(url_key, url) VALUES(?, ?)`, 26 | handler: func(_ context.Context, conn *connection, args []driver.NamedValue) (driver.Rows, error) { 27 | conn.storage.Store(args[0].Value, args[1].Value) 28 | return nil, nil 29 | }, 30 | }, 31 | { 32 | query: `SELECT url FROM redirects WHERE url_key = ?`, 33 | handler: func(_ context.Context, conn *connection, args []driver.NamedValue) (driver.Rows, error) { 34 | return &rows{ 35 | columns: []string{"url"}, 36 | rows: func() [][]driver.Value { 37 | if url, ok := conn.storage.Load(args[0].Value); ok { 38 | return [][]driver.Value{ 39 | {url.(string)}, 40 | } 41 | } 42 | return make([][]driver.Value, 0) 43 | }(), 44 | }, nil 45 | }, 46 | }, 47 | { 48 | query: `SELECT VERSION()`, 49 | handler: func(_ context.Context, _ *connection, _ []driver.NamedValue) (driver.Rows, error) { 50 | return &rows{ 51 | columns: []string{"version"}, 52 | rows: [][]driver.Value{ 53 | {"0.0.1"}, 54 | }, 55 | }, nil 56 | }, 57 | }, 58 | } 59 | -------------------------------------------------------------------------------- /examples/url-shortener-app/third_party/errgroup/errgroup.go: -------------------------------------------------------------------------------- 1 | package errgroup 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type Group struct { 8 | wg sync.WaitGroup 9 | errOnce sync.Once 10 | err error 11 | } 12 | 13 | func (g *Group) Wait() error { 14 | g.wg.Wait() 15 | return g.err 16 | } 17 | 18 | func (g *Group) Go(fn func() error) { 19 | g.wg.Add(1) 20 | go func() { 21 | defer g.wg.Done() 22 | if err := fn(); err != nil { 23 | g.errOnce.Do(func() { 24 | g.err = err 25 | }) 26 | } 27 | }() 28 | } 29 | -------------------------------------------------------------------------------- /examples/url-shortener-app/third_party/servermux/router.go: -------------------------------------------------------------------------------- 1 | package servermux 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type AddHandler func(pattern string, handler func(http.ResponseWriter, *http.Request)) 8 | 9 | type router struct { 10 | base *http.ServeMux 11 | } 12 | 13 | func CreateRouter() (http.HandlerFunc, AddHandler) { 14 | r := &router{ 15 | base: http.NewServeMux(), 16 | } 17 | return r.ServeHTTP, r.base.HandleFunc 18 | } 19 | 20 | func (r *router) ServeHTTP(response http.ResponseWriter, request *http.Request) { 21 | defer func() { 22 | if err := recover(); err != nil { 23 | http.Error(response, "Internal Server Error", http.StatusInternalServerError) 24 | } 25 | }() 26 | r.base.ServeHTTP(response, request) 27 | } 28 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/unpleasantcam/componego 2 | 3 | go 1.22 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unpleasantcam/componego/ca9559039e475ae00542874c1dc4fe462477df7d/go.sum -------------------------------------------------------------------------------- /impl/application/helpers.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package application 18 | 19 | import ( 20 | "github.com/unpleasantcam/componego" 21 | ) 22 | 23 | func ExitWrapper(err error) (int, error) { 24 | if err == nil { 25 | return componego.SuccessExitCode, nil 26 | } 27 | return componego.ErrorExitCode, err 28 | } 29 | -------------------------------------------------------------------------------- /impl/application/io.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package application 18 | 19 | import ( 20 | "io" 21 | 22 | "github.com/unpleasantcam/componego" 23 | ) 24 | 25 | type inputOutput struct { 26 | inputReader io.Reader 27 | outputWriter io.Writer 28 | errorOutputWriter io.Writer 29 | } 30 | 31 | func NewIO( 32 | inputReader io.Reader, 33 | outputWriter io.Writer, 34 | errorOutputWriter io.Writer, 35 | ) componego.ApplicationIO { 36 | return &inputOutput{ 37 | inputReader: inputReader, 38 | outputWriter: outputWriter, 39 | errorOutputWriter: errorOutputWriter, 40 | } 41 | } 42 | 43 | func (c *inputOutput) InputReader() io.Reader { 44 | return c.inputReader 45 | } 46 | 47 | func (c *inputOutput) OutputWriter() io.Writer { 48 | return c.outputWriter 49 | } 50 | 51 | func (c *inputOutput) ErrorOutputWriter() io.Writer { 52 | return c.errorOutputWriter 53 | } 54 | -------------------------------------------------------------------------------- /impl/application/tests/helpers_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "errors" 21 | "testing" 22 | 23 | "github.com/unpleasantcam/componego" 24 | "github.com/unpleasantcam/componego/impl/application" 25 | "github.com/unpleasantcam/componego/internal/testing/require" 26 | ) 27 | 28 | func TestExitWrapper(t *testing.T) { 29 | errCustom := errors.New("custom error") 30 | exitCode, err := application.ExitWrapper(errCustom) 31 | require.Equal(t, componego.ErrorExitCode, exitCode) 32 | require.ErrorIs(t, err, errCustom) 33 | exitCode, err = application.ExitWrapper(nil) 34 | require.Equal(t, componego.SuccessExitCode, exitCode) 35 | require.NoError(t, err) 36 | } 37 | -------------------------------------------------------------------------------- /impl/driver/options.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package driver 18 | 19 | import ( 20 | "context" 21 | "os" 22 | 23 | "github.com/unpleasantcam/componego" 24 | "github.com/unpleasantcam/componego/impl/application" 25 | "github.com/unpleasantcam/componego/impl/environment" 26 | "github.com/unpleasantcam/componego/impl/environment/managers/component" 27 | "github.com/unpleasantcam/componego/impl/environment/managers/config" 28 | "github.com/unpleasantcam/componego/impl/environment/managers/dependency" 29 | "github.com/unpleasantcam/componego/impl/environment/managers/dependency/container" 30 | "github.com/unpleasantcam/componego/internal/system" 31 | ) 32 | 33 | type ( 34 | initializer = func(env componego.Environment, options any) (canceller, error) 35 | canceller = func() error 36 | ) 37 | 38 | type Options struct { 39 | ConfigProviderFactory func() (componego.ConfigProvider, initializer) 40 | ComponentProviderFactory func() (componego.ComponentProvider, initializer) 41 | DependencyInvokerFactory func() (componego.DependencyInvoker, initializer) 42 | EnvironmentFactory func( 43 | context context.Context, 44 | application componego.Application, 45 | applicationIO componego.ApplicationIO, 46 | applicationMode componego.ApplicationMode, 47 | configProvider componego.ConfigProvider, 48 | componentProvider componego.ComponentProvider, 49 | dependencyInvoker componego.DependencyInvoker, 50 | ) componego.Environment 51 | AppIO componego.ApplicationIO 52 | Additional any 53 | } 54 | 55 | func Configure(options *Options) *Options { 56 | if options == nil { 57 | options = &Options{} 58 | } 59 | if options.ConfigProviderFactory == nil { 60 | options.ConfigProviderFactory = newConfigFactory 61 | } 62 | if options.ComponentProviderFactory == nil { 63 | options.ComponentProviderFactory = newComponentProviderFactory 64 | } 65 | if options.DependencyInvokerFactory == nil { 66 | options.DependencyInvokerFactory = newDependencyInvokerFactory 67 | } 68 | if options.EnvironmentFactory == nil { 69 | options.EnvironmentFactory = environment.New 70 | } 71 | if options.AppIO == nil { 72 | options.AppIO = application.NewIO(system.Stdin, system.Stdout, system.Stderr) 73 | } 74 | if options.Additional == nil { 75 | // This variable can contain any data depending on how the application is started. 76 | // By default, these are command line arguments. 77 | options.Additional = os.Args 78 | } 79 | return options 80 | } 81 | 82 | func newComponentProviderFactory() (componego.ComponentProvider, initializer) { 83 | manager, initializer := component.NewManager() 84 | return manager, func(env componego.Environment, _ any) (canceller, error) { 85 | components, err := component.ExtractComponents(env.Application()) 86 | if err != nil { 87 | return nil, err 88 | } 89 | return nil, initializer(components) 90 | } 91 | } 92 | 93 | func newDependencyInvokerFactory() (componego.DependencyInvoker, initializer) { 94 | manager, initializer := dependency.NewManager() 95 | return manager, func(env componego.Environment, _ any) (canceller, error) { 96 | dependencies, err := dependency.ExtractDependencies(env) 97 | if err != nil { 98 | return nil, err 99 | } 100 | containerInstance, containerInitializer := container.New(len(dependencies)) 101 | // There may be a recursive call to the container through the dependency manager 102 | // during the initialization of dependencies inside the container. 103 | if err = initializer(containerInstance); err != nil { 104 | return nil, err 105 | } 106 | return containerInitializer(dependencies) 107 | } 108 | } 109 | 110 | func newConfigFactory() (componego.ConfigProvider, initializer) { 111 | manager, initializer := config.NewManager() 112 | return manager, func(env componego.Environment, options any) (canceller, error) { 113 | parsedConfig, err := config.ParseConfig(env, options) 114 | if err != nil { 115 | return nil, err 116 | } 117 | return nil, initializer(env, parsedConfig) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /impl/driver/tests/driver_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "errors" 21 | "testing" 22 | 23 | "github.com/unpleasantcam/componego/impl/driver" 24 | "github.com/unpleasantcam/componego/internal/testing/require" 25 | "github.com/unpleasantcam/componego/internal/testing/types" 26 | ) 27 | 28 | func TestDriver(t *testing.T) { 29 | DriverTester[*testing.T](t, func() driver.Driver { 30 | return driver.New(nil) 31 | }) 32 | } 33 | 34 | func TestErrorRecoveryOnStop(t *testing.T) { 35 | t.Run("nil recovery without the previous error", func(t *testing.T) { 36 | resultErr := driver.ErrorRecoveryOnStop(nil, nil) 37 | require.NoError(t, resultErr) 38 | }) 39 | 40 | t.Run("nil recovery with the previous error", func(t *testing.T) { 41 | prevErr := errors.New("previous error") 42 | resultErr := driver.ErrorRecoveryOnStop(nil, prevErr) 43 | require.NotErrorIs(t, resultErr, driver.ErrPanic) 44 | require.ErrorIs(t, resultErr, prevErr) 45 | }) 46 | 47 | t.Run("recovery as an error without the previous error", func(t *testing.T) { 48 | panicErr := errors.New("panic occurred") 49 | resultErr := driver.ErrorRecoveryOnStop(panicErr, nil) 50 | require.ErrorIs(t, resultErr, driver.ErrPanic) 51 | require.ErrorIs(t, resultErr, panicErr) 52 | }) 53 | 54 | t.Run("recovery as a string without the previous error", func(t *testing.T) { 55 | resultErr := driver.ErrorRecoveryOnStop("panic occurred", nil) 56 | require.ErrorIs(t, resultErr, driver.ErrPanic) 57 | require.ErrorContains(t, resultErr, "panic occurred") 58 | }) 59 | 60 | t.Run("recovery as a custom string without the previous error", func(t *testing.T) { 61 | resultErr := driver.ErrorRecoveryOnStop(types.CustomString("panic occurred"), nil) 62 | require.ErrorIs(t, resultErr, driver.ErrPanic) 63 | require.ErrorContains(t, resultErr, "panic occurred") 64 | }) 65 | 66 | t.Run("recovery as a custom type without the previous error", func(t *testing.T) { 67 | resultErr := driver.ErrorRecoveryOnStop(types.AStruct{}, nil) 68 | require.ErrorIs(t, resultErr, driver.ErrUnknownPanic) 69 | }) 70 | 71 | t.Run("recovery as an error with the previous error", func(t *testing.T) { 72 | prevErr := errors.New("previous error") 73 | panicErr := errors.New("panic occurred") 74 | resultErr := driver.ErrorRecoveryOnStop(panicErr, prevErr) 75 | require.ErrorIs(t, resultErr, prevErr) 76 | require.ErrorIs(t, resultErr, driver.ErrPanic) 77 | }) 78 | 79 | t.Run("recovery as a string with the previous error", func(t *testing.T) { 80 | prevErr := errors.New("previous error") 81 | resultErr := driver.ErrorRecoveryOnStop("panic occurred", prevErr) 82 | require.ErrorIs(t, resultErr, driver.ErrPanic) 83 | require.ErrorIs(t, resultErr, prevErr) 84 | require.ErrorContains(t, resultErr, "panic occurred") 85 | }) 86 | 87 | t.Run("recovery as a custom string with the previous error", func(t *testing.T) { 88 | prevErr := errors.New("previous error") 89 | resultErr := driver.ErrorRecoveryOnStop(types.CustomString("panic occurred"), prevErr) 90 | require.ErrorIs(t, resultErr, driver.ErrPanic) 91 | require.ErrorIs(t, resultErr, prevErr) 92 | require.ErrorContains(t, resultErr, "panic occurred") 93 | }) 94 | 95 | t.Run("recovery as a custom type with the previous error", func(t *testing.T) { 96 | prevErr := errors.New("previous error") 97 | resultErr := driver.ErrorRecoveryOnStop(types.AStruct{}, prevErr) 98 | require.ErrorIs(t, resultErr, driver.ErrUnknownPanic) 99 | require.ErrorIs(t, resultErr, prevErr) 100 | }) 101 | } 102 | -------------------------------------------------------------------------------- /impl/environment/managers/config/helper.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "regexp" 23 | 24 | "github.com/unpleasantcam/componego" 25 | ) 26 | 27 | func Get[T any](configKey string, processor componego.Processor, env componego.Environment) (T, error) { 28 | value, err := env.ConfigProvider().ConfigValue(configKey, processor) 29 | if err != nil { 30 | return *new(T), err 31 | } 32 | if result, ok := value.(T); ok { 33 | return result, nil 34 | } 35 | return *new(T), fmt.Errorf("could not convert the value for the '%s' to type %T", configKey, *new(T)) 36 | } 37 | 38 | func GetOrPanic[T any](configKey string, processor componego.Processor, env componego.Environment) T { 39 | result, err := Get[T](configKey, processor, env) 40 | if err != nil { 41 | panic(err) 42 | } 43 | return result 44 | } 45 | 46 | func ProcessVariables(settings map[string]any) (err error) { 47 | // ${ENV:VARIABLE_NAME} or ${ENV:VARIABLE_NAME|DEFAULT_VALUE} 48 | variableRegex := regexp.MustCompile(`\$\{ENV:([a-zA-Z0-9_]+)(\|([a-zA-Z0-9_]+))?}`) 49 | for key, value := range settings { 50 | valueAsString, ok := value.(string) 51 | if !ok { 52 | // We process nested values since the configuration nesting level can be any. 53 | valueAsMap, ok := value.(map[string]any) 54 | if !ok { 55 | continue 56 | } 57 | if err = ProcessVariables(valueAsMap); err != nil { 58 | return err 59 | } 60 | continue 61 | } 62 | settings[key] = variableRegex.ReplaceAllStringFunc(valueAsString, func(match string) string { 63 | matches := variableRegex.FindStringSubmatch(match) 64 | if envValue := os.Getenv(matches[1]); len(envValue) != 0 { 65 | return envValue 66 | } else if len(matches[3]) > 0 { 67 | return matches[3] // default value 68 | } 69 | err = fmt.Errorf("environment variable '%s' not found", matches[1]) 70 | return match 71 | }) 72 | if err != nil { 73 | break 74 | } 75 | } 76 | return err 77 | } 78 | -------------------------------------------------------------------------------- /impl/environment/managers/config/manager.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | import ( 20 | "strings" 21 | 22 | "github.com/unpleasantcam/componego" 23 | "github.com/unpleasantcam/componego/libs/xerrors" 24 | ) 25 | 26 | const ( 27 | // delimiter is a constant which specifies which delimiter is used in configuration keys. 28 | delimiter = "." 29 | ) 30 | 31 | var ( 32 | ErrConfigManager = xerrors.New("error inside config manager", "E0310") 33 | ErrConfigInit = ErrConfigManager.WithMessage("config init error", "E0311") 34 | ErrConfigGet = ErrConfigManager.WithMessage("config get error", "E0312") 35 | ErrValueNotFound = ErrConfigGet.WithMessage("config value not found", "E0313") 36 | ) 37 | 38 | type manager struct { 39 | env componego.Environment 40 | parsedConfig map[string]any 41 | } 42 | 43 | func NewManager() (componego.ConfigProvider, func(componego.Environment, map[string]any) error) { 44 | m := &manager{} 45 | return m, m.initialize 46 | } 47 | 48 | func (m *manager) ConfigValue(configKey string, processor componego.Processor) (any, error) { 49 | value, ok := m.extractValue(configKey) 50 | if processor == nil { 51 | if ok { 52 | return value, nil 53 | } 54 | return nil, ErrValueNotFound.WithOptions("E0314", 55 | xerrors.NewOption("componego:config:key", configKey), 56 | ) 57 | } 58 | // Injecting a dependency into an object before calling the object's methods. 59 | err := m.env.DependencyInvoker().PopulateFields(processor) 60 | if err == nil { 61 | value, err = processor.ProcessData(value) 62 | if err == nil { 63 | return value, nil 64 | } 65 | } 66 | return nil, ErrConfigGet.WithError(err, "E0315", 67 | xerrors.NewOption("componego:config:key", configKey), 68 | ) 69 | } 70 | 71 | func (m *manager) extractValue(configKey string) (any, bool) { 72 | if value, ok := m.parsedConfig[configKey]; ok { 73 | return value, true 74 | } 75 | keys := strings.Split(configKey, delimiter) 76 | var configValue any = m.parsedConfig 77 | for _, key := range keys { 78 | switch parsedConfig := configValue.(type) { 79 | case map[string]any: 80 | if value, ok := parsedConfig[key]; ok { 81 | configValue = value 82 | } else { 83 | return nil, false 84 | } 85 | } 86 | } 87 | return configValue, true 88 | } 89 | 90 | func (m *manager) initialize(env componego.Environment, parsedConfig map[string]any) error { 91 | m.env = env 92 | m.parsedConfig = parsedConfig 93 | return nil 94 | } 95 | 96 | func ParseConfig(env componego.Environment, options any) (map[string]any, error) { 97 | if app, ok := env.Application().(componego.ApplicationConfigInit); ok { 98 | parsedConfig, err := app.ApplicationConfigInit(env.ApplicationMode(), options) 99 | if err != nil { 100 | return nil, ErrConfigInit.WithError(err, "E0316") 101 | } 102 | return parsedConfig, nil 103 | } 104 | return nil, nil 105 | } 106 | -------------------------------------------------------------------------------- /impl/environment/managers/dependency/container/tests/container_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/unpleasantcam/componego" 23 | "github.com/unpleasantcam/componego/impl/environment/managers/dependency/container" 24 | ) 25 | 26 | func TestDependencyContainer(t *testing.T) { 27 | DependencyContainerTester[*testing.T](t, func() (container.Container, func([]componego.Dependency) (func() error, error)) { 28 | return container.New(5) 29 | }) 30 | } 31 | 32 | func BenchmarkDependencyContainerInitialize(b *testing.B) { 33 | factories := GenerateTestFactories(1000, 5) 34 | b.Run("dependency container initialize", func(b *testing.B) { 35 | b.ReportAllocs() 36 | for n := 0; n < b.N; n++ { 37 | _, initializer := container.New(len(factories)) 38 | if _, err := initializer(factories); err != nil { 39 | b.Fatal(err) 40 | } 41 | } 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /impl/environment/managers/dependency/helpers.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package dependency 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/unpleasantcam/componego" 23 | ) 24 | 25 | func Get[T any](env componego.Environment) (T, error) { 26 | value := *new(T) 27 | err := env.DependencyInvoker().Populate(&value) 28 | return value, err 29 | } 30 | 31 | func GetOrPanic[T any](env componego.Environment) T { 32 | value, err := Get[T](env) 33 | if err != nil { 34 | panic(err) 35 | } 36 | return value 37 | } 38 | 39 | func Invoke[T any](fn any, env componego.Environment) (T, error) { 40 | value, err := env.DependencyInvoker().Invoke(fn) 41 | if err != nil { 42 | return *new(T), err 43 | } 44 | if result, ok := value.(T); ok { 45 | return result, nil 46 | } 47 | return *new(T), fmt.Errorf("could not convert the returned value to type %T", *new(T)) 48 | } 49 | 50 | func InvokeOrPanic[T any](fn any, env componego.Environment) T { 51 | value, err := Invoke[T](fn, env) 52 | if err != nil { 53 | panic(err) 54 | } 55 | return value 56 | } 57 | -------------------------------------------------------------------------------- /impl/environment/managers/dependency/tests/manager_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/unpleasantcam/componego/impl/environment/managers/dependency" 23 | ) 24 | 25 | func TestDependencyManager(t *testing.T) { 26 | DependencyManagerTester[*testing.T](t, dependency.NewManager) 27 | } 28 | -------------------------------------------------------------------------------- /impl/environment/tests/environment.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/unpleasantcam/componego" 23 | "github.com/unpleasantcam/componego/impl/application" 24 | "github.com/unpleasantcam/componego/impl/environment" 25 | "github.com/unpleasantcam/componego/impl/environment/managers/component" 26 | "github.com/unpleasantcam/componego/impl/environment/managers/config" 27 | "github.com/unpleasantcam/componego/impl/environment/managers/dependency" 28 | "github.com/unpleasantcam/componego/internal/system" 29 | "github.com/unpleasantcam/componego/internal/testing" 30 | "github.com/unpleasantcam/componego/internal/testing/require" 31 | ) 32 | 33 | type testCtxKey struct{} 34 | 35 | func EnvironmentTester[T testing.TRun[T]]( 36 | t testing.TRun[T], 37 | factory func( 38 | context context.Context, 39 | application componego.Application, 40 | applicationIO componego.ApplicationIO, 41 | applicationMode componego.ApplicationMode, 42 | configProvider componego.ConfigProvider, 43 | componentProvider componego.ComponentProvider, 44 | dependencyInvoker componego.DependencyInvoker, 45 | ) componego.Environment, 46 | ) { 47 | components := []componego.Component{ 48 | component.NewFactory("tests:component1", "0.0.1").Build(), 49 | component.NewFactory("tests:component2", "0.0.2").Build(), 50 | } 51 | app := application.NewFactory("Test Application").Build() 52 | appIO := application.NewIO(system.Stdin, system.Stdout, system.Stderr) 53 | appMode := componego.ProductionMode 54 | configProvider, _ := config.NewManager() 55 | componentProvider, componentsInitializer := component.NewManager() 56 | require.NoError(t, componentsInitializer(components)) 57 | dependencyInvoker, _ := dependency.NewManager() 58 | 59 | ctxKey := testCtxKey{} 60 | 61 | t.Run("get objects", func(t T) { 62 | ctx := context.WithValue(context.Background(), ctxKey, 123) 63 | env := factory(ctx, app, appIO, appMode, configProvider, componentProvider, dependencyInvoker) 64 | require.Same(t, app, env.Application()) 65 | require.Same(t, appIO, env.ApplicationIO()) 66 | require.Same(t, configProvider, env.ConfigProvider()) 67 | require.Same(t, dependencyInvoker, env.DependencyInvoker()) 68 | require.Equal(t, 123, env.GetContext().Value(ctxKey)) 69 | require.Equal(t, appMode, env.ApplicationMode()) 70 | require.Equal(t, componentProvider.Components(), env.Components()) 71 | }) 72 | 73 | t.Run("set context", func(t T) { 74 | env := factory(context.Background(), app, appIO, appMode, configProvider, componentProvider, dependencyInvoker) 75 | err := env.SetContext(context.Background()) 76 | require.ErrorIs(t, err, environment.ErrInvalidParentContext) 77 | err = env.SetContext(env.GetContext()) 78 | require.NoError(t, err) 79 | err = env.SetContext(context.WithValue(env.GetContext(), ctxKey, 321)) 80 | require.NoError(t, err) 81 | }) 82 | 83 | t.Run("get environment from context", func(t T) { 84 | expectedEnv := factory(context.Background(), app, appIO, appMode, configProvider, componentProvider, dependencyInvoker) 85 | 86 | t.Run("without panic", func(t T) { 87 | actualEnv, err := environment.GetEnvironment(expectedEnv.GetContext()) 88 | require.NoError(t, err) 89 | require.Same(t, expectedEnv, actualEnv) 90 | actualEnv, err = environment.GetEnvironment(context.Background()) 91 | require.ErrorIs(t, err, environment.ErrNoEnvironmentInContext) 92 | require.Nil(t, actualEnv) 93 | }) 94 | 95 | t.Run("with panic", func(t T) { 96 | require.NotPanics(t, func() { 97 | actualEnv := environment.GetEnvironmentOrPanic(expectedEnv.GetContext()) 98 | require.Same(t, expectedEnv, actualEnv) 99 | }) 100 | require.PanicsWithError(t, environment.ErrNoEnvironmentInContext.Error(), func() { 101 | environment.GetEnvironmentOrPanic(context.Background()) 102 | }) 103 | }) 104 | }) 105 | } 106 | -------------------------------------------------------------------------------- /impl/environment/tests/environment_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/unpleasantcam/componego/impl/environment" 23 | ) 24 | 25 | func TestEnvironment(t *testing.T) { 26 | EnvironmentTester[*testing.T](t, environment.New) 27 | } 28 | -------------------------------------------------------------------------------- /impl/processors/processor.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package processors 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/unpleasantcam/componego" 23 | ) 24 | 25 | type processor struct { 26 | handler func(value any) (any, error) 27 | } 28 | 29 | func New(handler func(value any) (any, error)) componego.Processor { 30 | return &processor{ 31 | handler: handler, 32 | } 33 | } 34 | 35 | func (p *processor) ProcessData(value any) (any, error) { 36 | return p.handler(value) 37 | } 38 | 39 | type multiProcessor struct { 40 | di componego.DependencyInvoker `componego:"inject"` 41 | processors []componego.Processor 42 | } 43 | 44 | func Multi(processors ...componego.Processor) componego.Processor { 45 | return &multiProcessor{ 46 | processors: processors, 47 | } 48 | } 49 | 50 | func (m *multiProcessor) ProcessData(value any) (any, error) { 51 | for _, item := range m.processors { 52 | if err := m.di.PopulateFields(item); err != nil { 53 | return nil, err 54 | } else if value, err = item.ProcessData(value); err != nil { 55 | return nil, err 56 | } 57 | } 58 | return value, nil 59 | } 60 | 61 | func DefaultValue(value any) componego.Processor { 62 | return New(func(prevValue any) (any, error) { 63 | if prevValue == nil { 64 | return value, nil 65 | } 66 | return prevValue, nil 67 | }) 68 | } 69 | 70 | func IsRequired() componego.Processor { 71 | return New(func(value any) (any, error) { 72 | if value == nil { 73 | return nil, fmt.Errorf("the value is required") 74 | } 75 | return value, nil 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /impl/processors/tests/types_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/unpleasantcam/componego/impl/processors" 23 | "github.com/unpleasantcam/componego/internal/testing/require" 24 | "github.com/unpleasantcam/componego/libs/type-cast" 25 | ) 26 | 27 | func TestToBool(t *testing.T) { 28 | processor := processors.ToBool() 29 | for _, testCase := range getTypeCastCases() { 30 | expectedValue, expectedErr := type_cast.ToBool(testCase) 31 | actualValue, actualErr := processor.ProcessData(testCase) 32 | if expectedErr == nil { 33 | require.NoError(t, actualErr) 34 | } else { 35 | require.EqualError(t, actualErr, expectedErr.Error()) 36 | } 37 | require.NotPanics(t, func() { 38 | require.Equal(t, expectedValue, actualValue.(bool)) 39 | }) 40 | } 41 | } 42 | 43 | func TestIsBool(t *testing.T) { 44 | processor := processors.IsBool() 45 | for _, testCase := range getTypeCastCases() { 46 | _, isBool := testCase.(bool) 47 | expectedValue := testCase 48 | actualValue, actualErr := processor.ProcessData(testCase) 49 | if isBool { 50 | require.NoError(t, actualErr) 51 | require.Equal(t, expectedValue, actualValue) 52 | } else { 53 | require.Error(t, actualErr) 54 | require.Nil(t, actualValue) 55 | } 56 | } 57 | } 58 | 59 | func TestToInt64(t *testing.T) { 60 | processor := processors.ToInt64() 61 | for _, testCase := range getTypeCastCases() { 62 | expectedValue, expectedErr := type_cast.ToInt64(testCase) 63 | actualValue, actualErr := processor.ProcessData(testCase) 64 | if expectedErr == nil { 65 | require.NoError(t, actualErr) 66 | } else { 67 | require.EqualError(t, actualErr, expectedErr.Error()) 68 | } 69 | require.NotPanics(t, func() { 70 | require.Equal(t, expectedValue, actualValue.(int64)) 71 | }) 72 | } 73 | } 74 | 75 | func TestToFloat64(t *testing.T) { 76 | processor := processors.ToFloat64() 77 | for _, testCase := range getTypeCastCases() { 78 | expectedValue, expectedErr := type_cast.ToFloat64(testCase) 79 | actualValue, actualErr := processor.ProcessData(testCase) 80 | if expectedErr == nil { 81 | require.NoError(t, actualErr) 82 | } else { 83 | require.EqualError(t, actualErr, expectedErr.Error()) 84 | } 85 | require.NotPanics(t, func() { 86 | require.Equal(t, expectedValue, actualValue.(float64)) 87 | }) 88 | } 89 | } 90 | 91 | func TestToString(t *testing.T) { 92 | processor := processors.ToString() 93 | for _, testCase := range getTypeCastCases() { 94 | expectedValue, expectedErr := type_cast.ToString(testCase) 95 | actualValue, actualErr := processor.ProcessData(testCase) 96 | if expectedErr == nil { 97 | require.NoError(t, actualErr) 98 | } else { 99 | require.EqualError(t, actualErr, expectedErr.Error()) 100 | } 101 | require.NotPanics(t, func() { 102 | require.Equal(t, expectedValue, actualValue.(string)) 103 | }) 104 | } 105 | } 106 | 107 | func getTypeCastCases() []any { 108 | // noinspection ALL 109 | return []any{ 110 | nil, 111 | true, 112 | false, 113 | 123, 114 | 0, 115 | int8(123), 116 | int8(0), 117 | int16(123), 118 | int16(0), 119 | int32(123), 120 | int32(0), 121 | int64(123), 122 | int64(0), 123 | uint8(123), 124 | uint8(0), 125 | uint16(123), 126 | uint16(0), 127 | uint32(123), 128 | uint32(0), 129 | uint64(123), 130 | uint64(0), 131 | float32(0.0), 132 | float32(123.123456789), 133 | float64(0.0), 134 | float64(123.123456789), 135 | "true", 136 | "false", 137 | "TRUE", 138 | "FALSE", 139 | "1", 140 | "0", 141 | "-1", 142 | "", 143 | "string", 144 | struct{}{}, 145 | struct{ x int }{x: 123}, 146 | []int{1, 2, 3}, 147 | [...]string{"1", "2", "3"}, 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /impl/processors/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package processors 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/unpleasantcam/componego" 23 | "github.com/unpleasantcam/componego/libs/type-cast" 24 | ) 25 | 26 | // ToBool converts the value to boolean. 27 | func ToBool() componego.Processor { 28 | return New(func(value any) (any, error) { 29 | return type_cast.ToBool(value) 30 | }) 31 | } 32 | 33 | // IsBool checks whether a value is a boolean value. 34 | func IsBool() componego.Processor { 35 | return New(func(value any) (any, error) { 36 | if _, ok := value.(bool); ok { 37 | return value, nil 38 | } 39 | return nil, fmt.Errorf("the value is not a boolean") 40 | }) 41 | } 42 | 43 | // ToInt64 converts the value to int64. 44 | func ToInt64() componego.Processor { 45 | return New(func(value any) (any, error) { 46 | return type_cast.ToInt64(value) 47 | }) 48 | } 49 | 50 | // ToFloat64 converts the value to float64. 51 | func ToFloat64() componego.Processor { 52 | return New(func(value any) (any, error) { 53 | return type_cast.ToFloat64(value) 54 | }) 55 | } 56 | 57 | // ToString converts the value to string. 58 | func ToString() componego.Processor { 59 | return New(func(value any) (any, error) { 60 | return type_cast.ToString(value) 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /impl/runner/runner.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package runner 18 | 19 | import ( 20 | "context" 21 | "os" 22 | "os/signal" 23 | "runtime" 24 | "syscall" 25 | 26 | "github.com/unpleasantcam/componego" 27 | "github.com/unpleasantcam/componego/impl/driver" 28 | "github.com/unpleasantcam/componego/impl/runner/unhandled-errors" 29 | "github.com/unpleasantcam/componego/internal/developer" 30 | "github.com/unpleasantcam/componego/internal/system" 31 | "github.com/unpleasantcam/componego/internal/utils" 32 | ) 33 | 34 | // RunWithContext runs the application with context and returns the exit code. 35 | func RunWithContext(ctx context.Context, app componego.Application, appMode componego.ApplicationMode) int { 36 | d := driver.New(nil) 37 | exitCode, err := d.RunApplication(ctx, app, appMode) 38 | if err != nil { 39 | // Here we display all errors that were not processed. 40 | utils.Fprint(system.Stderr, unhandled_errors.ToString(err, appMode, unhandled_errors.GetHandlers())) 41 | } 42 | return exitCode 43 | } 44 | 45 | // Run runs the application and returns the exit code. 46 | func Run(app componego.Application, appMode componego.ApplicationMode) int { 47 | return RunWithContext(context.Background(), app, appMode) 48 | } 49 | 50 | // RunAndExit runs the application and exits the program after stopping the application. 51 | func RunAndExit(app componego.Application, appMode componego.ApplicationMode) { 52 | exitCode := Run(app, appMode) 53 | exit(exitCode, appMode) 54 | } 55 | 56 | // RunGracefullyAndExit runs the application and stops it gracefully. 57 | // This function cancels the main context of the application, 58 | // so the process of stopping the application depends on your application code. 59 | func RunGracefullyAndExit(app componego.Application, appMode componego.ApplicationMode) { 60 | cancelableCtx, cancelCtx := context.WithCancel(context.Background()) 61 | defer cancelCtx() // This will cancel the context if panic occurs. 62 | interruptChan := make(chan os.Signal, 1) 63 | signal.Notify(interruptChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) 64 | go func() { 65 | select { 66 | case <-interruptChan: 67 | case <-cancelableCtx.Done(): 68 | } 69 | signal.Stop(interruptChan) 70 | cancelCtx() 71 | }() 72 | exitCode := RunWithContext(cancelableCtx, app, appMode) 73 | cancelCtx() // This will cancel the context unless panic occurs. 74 | runtime.Gosched() // We switch the runtime so that waiting goroutines can complete their work. 75 | exit(exitCode, appMode) 76 | } 77 | 78 | func exit(exitCode int, appMode componego.ApplicationMode) { 79 | if appMode == componego.DeveloperMode && system.NumGoroutineBeforeExit() > 1 { 80 | // In any case, all goroutines will be terminated after exiting the application, but we will show this message. 81 | developer.Warning(system.Stdout, "The application was stopped, but goroutines were still running.") 82 | developer.Warning(system.Stdout, "So it may not be stopped correctly. In some cases, this notification may be false.") 83 | developer.Warning(system.Stdout, "Read more here https://componego.github.io/warnings/goroutine-leak") 84 | } 85 | // Make sure you call this function in the root goroutine to ensure the program exits correctly. 86 | system.Exit(exitCode) 87 | } 88 | -------------------------------------------------------------------------------- /impl/runner/unhandled-errors/handlers/vendor-proxy.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package handlers 18 | 19 | import ( 20 | "errors" 21 | "io" 22 | 23 | "github.com/unpleasantcam/componego" 24 | "github.com/unpleasantcam/componego/internal/utils" 25 | "github.com/unpleasantcam/componego/libs/vendor-proxy" 26 | ) 27 | 28 | func VendorProxyHandler(err error, writer io.Writer, appMode componego.ApplicationMode) bool { 29 | if !errors.Is(err, vendor_proxy.ErrFunctionNotExists) { 30 | return false 31 | } 32 | DefaultHandler(err, writer, appMode) 33 | utils.Fprint(writer, `--- 34 | Perhaps, you need to add the following lines to your main package. 35 | 36 | import ( 37 | _ "github.com/componego/meta-package/pre-init/vendor-proxy/for-app" // if you run the application. 38 | _ "github.com/componego/meta-package/pre-init/vendor-proxy/for-tests" // if you run the tests. 39 | ) 40 | 41 | If that doesn't help, then you need to debug the code. 42 | `) 43 | return true 44 | } 45 | -------------------------------------------------------------------------------- /impl/runner/unhandled-errors/render.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package unhandled_errors 18 | 19 | import ( 20 | "fmt" 21 | "io" 22 | "runtime/debug" 23 | "strings" 24 | 25 | "github.com/unpleasantcam/componego" 26 | "github.com/unpleasantcam/componego/impl/runner/unhandled-errors/handlers" 27 | "github.com/unpleasantcam/componego/libs/ordered-map" 28 | ) 29 | 30 | type handler = func(err error, writer io.Writer, appMode componego.ApplicationMode) bool 31 | 32 | func GetHandlers() ordered_map.Map[string, handler] { 33 | result := ordered_map.New[string, handler](3) 34 | result.Set("componego:vendor-proxy", handlers.VendorProxyHandler) 35 | return result 36 | } 37 | 38 | func ToString(err error, appMode componego.ApplicationMode, errHandlers ordered_map.Map[string, handler]) (message string) { 39 | defer func() { 40 | if r := recover(); r != nil { 41 | stack := string(debug.Stack()) 42 | message = fmt.Sprintf("panic during rendering the original error: %v\n\nstack: %s\n", r, stack) 43 | } 44 | }() 45 | writer := &strings.Builder{} 46 | for _, fn := range errHandlers.Values() { 47 | if fn(err, writer, appMode) { 48 | return writer.String() 49 | } 50 | } 51 | handlers.DefaultHandler(err, writer, appMode) 52 | return writer.String() 53 | } 54 | -------------------------------------------------------------------------------- /internal/developer/message.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package developer 18 | 19 | import ( 20 | "io" 21 | 22 | "github.com/unpleasantcam/componego/internal/utils" 23 | "github.com/unpleasantcam/componego/libs/color" 24 | ) 25 | 26 | func Warning(writer io.Writer, args ...any) { 27 | writer = color.NewColoredWriter(writer, color.WhiteColor, color.YellowBackground) 28 | utils.Fprint(writer, "[W] ") 29 | utils.Fprintln(writer, args...) 30 | } 31 | -------------------------------------------------------------------------------- /internal/developer/tests/message_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "bytes" 21 | "testing" 22 | 23 | "github.com/unpleasantcam/componego/internal/developer" 24 | "github.com/unpleasantcam/componego/internal/testing/require" 25 | ) 26 | 27 | func TestWarning(t *testing.T) { 28 | t.Run("basic test", func(t *testing.T) { 29 | buffer := &bytes.Buffer{} 30 | developer.Warning(buffer, "hello") 31 | require.Contains(t, buffer.String(), "hello") 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /internal/system/os.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package system 18 | 19 | import ( 20 | "io" 21 | "os" 22 | ) 23 | 24 | var ( 25 | Stdin io.Reader = os.Stdin 26 | Stdout io.Writer = os.Stdout 27 | Stderr io.Writer = os.Stderr 28 | Exit = os.Exit 29 | ) 30 | -------------------------------------------------------------------------------- /internal/system/runtime.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package system 18 | 19 | import ( 20 | "os" 21 | "os/signal" 22 | "runtime" 23 | "syscall" 24 | ) 25 | 26 | func NumGoroutineBeforeExit() int { 27 | numGoroutine := runtime.NumGoroutine() 28 | if numGoroutine == 1 { 29 | return numGoroutine 30 | } 31 | // Signals start a goroutine that never stops. 32 | // We make sure that this goroutine does not influence the result. 33 | interruptChan := make(chan os.Signal, 1) 34 | signal.Notify(interruptChan, syscall.SIGTERM) 35 | signal.Stop(interruptChan) 36 | return runtime.NumGoroutine() - 1 37 | } 38 | -------------------------------------------------------------------------------- /internal/system/tests/runtime_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "sync" 21 | "testing" 22 | 23 | "github.com/unpleasantcam/componego/internal/system" 24 | "github.com/unpleasantcam/componego/internal/testing/require" 25 | ) 26 | 27 | func TestNumGoroutineBeforeExit(t *testing.T) { 28 | t.Run("basic test", func(t *testing.T) { 29 | require.True(t, system.NumGoroutineBeforeExit() >= 1) 30 | numGoroutine := 0 31 | wg := sync.WaitGroup{} 32 | wg.Add(1) 33 | go func() { 34 | defer wg.Done() 35 | numGoroutine = system.NumGoroutineBeforeExit() 36 | }() 37 | wg.Wait() 38 | require.True(t, numGoroutine > 1) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /internal/testing/logger/logger.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package logger 18 | 19 | import ( 20 | "fmt" 21 | "io" 22 | "reflect" 23 | "strings" 24 | ) 25 | 26 | // LogData is a function that writes data to the log. 27 | func LogData(writer io.Writer, messages ...any) { 28 | if writer == nil { 29 | return 30 | } 31 | _, err := fmt.Fprintln(writer, messages...) 32 | if err != nil { 33 | panic(err) 34 | } 35 | } 36 | 37 | // ExpectedLogData is a function that returns formatted data. 38 | // You can use it with the LogData function to compare data in tests. 39 | func ExpectedLogData(lines ...any) string { 40 | var result strings.Builder 41 | for _, line := range lines { 42 | if line == nil { 43 | result.WriteString(fmt.Sprintln(line)) 44 | continue 45 | } 46 | reflectValue := reflect.ValueOf(line) 47 | if reflectValue.Kind() != reflect.Slice && reflectValue.Kind() != reflect.Array { 48 | result.WriteString(fmt.Sprintln(line)) 49 | continue 50 | } 51 | valueLen := reflectValue.Len() 52 | messages := make([]any, valueLen) 53 | for i := 0; i < valueLen; i++ { 54 | messages[i] = reflectValue.Index(i).Interface() 55 | } 56 | result.WriteString(fmt.Sprintln(messages...)) 57 | } 58 | return result.String() 59 | } 60 | -------------------------------------------------------------------------------- /internal/testing/logger/tests/logger_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "bytes" 21 | "fmt" 22 | "testing" 23 | 24 | "github.com/unpleasantcam/componego/internal/testing/logger" 25 | "github.com/unpleasantcam/componego/internal/testing/require" 26 | ) 27 | 28 | func TestLogData(t *testing.T) { 29 | t.Run("nil writer", func(t *testing.T) { 30 | require.NotPanics(t, func() { 31 | logger.LogData(nil, "hello") 32 | }) 33 | }) 34 | 35 | t.Run("single message", func(t *testing.T) { 36 | var buffer bytes.Buffer 37 | logger.LogData(&buffer, "hello") 38 | expected := fmt.Sprintln("hello") 39 | require.Equal(t, expected, buffer.String()) 40 | }) 41 | 42 | t.Run("multiple messages", func(t *testing.T) { 43 | var buffer bytes.Buffer 44 | logger.LogData(&buffer, "hello", 123, true) 45 | expected := fmt.Sprintln("hello", 123, true) 46 | require.Equal(t, expected, buffer.String()) 47 | }) 48 | 49 | t.Run("empty messages", func(t *testing.T) { 50 | var buffer bytes.Buffer 51 | logger.LogData(&buffer) 52 | expected := fmt.Sprintln() 53 | require.Equal(t, expected, buffer.String()) 54 | }) 55 | 56 | t.Run("error on write", func(t *testing.T) { 57 | writer := &errorWriter{} 58 | require.Panics(t, func() { 59 | logger.LogData(writer, "hello") 60 | }) 61 | }) 62 | } 63 | 64 | func TestExpectedLogData(t *testing.T) { 65 | t.Run("nil value", func(t *testing.T) { 66 | result := logger.ExpectedLogData(nil) 67 | expected := fmt.Sprintln(nil) 68 | require.Equal(t, expected, result) 69 | }) 70 | 71 | t.Run("non-slice value", func(t *testing.T) { 72 | result := logger.ExpectedLogData("hello") 73 | expected := fmt.Sprintln("hello") 74 | require.Equal(t, expected, result) 75 | }) 76 | 77 | t.Run("slice of strings", func(t *testing.T) { 78 | result := logger.ExpectedLogData([]string{"a", "b", "c"}) 79 | expected := fmt.Sprintln([]any{"a", "b", "c"}...) 80 | require.Equal(t, expected, result) 81 | }) 82 | 83 | t.Run("array of integers", func(t *testing.T) { 84 | result := logger.ExpectedLogData([...]int{1, 2, 3}) 85 | expected := fmt.Sprintln([]any{1, 2, 3}...) 86 | require.Equal(t, expected, result) 87 | }) 88 | 89 | t.Run("mixed values", func(t *testing.T) { 90 | result := logger.ExpectedLogData(nil, "hello", []int{1, 2, 3}, [...]string{"x", "y"}) 91 | expected := fmt.Sprintln(nil) + fmt.Sprintln("hello") + 92 | fmt.Sprintln([]any{1, 2, 3}...) + fmt.Sprintln([]any{"x", "y"}...) 93 | require.Equal(t, expected, result) 94 | }) 95 | 96 | t.Run("empty slice", func(t *testing.T) { 97 | result := logger.ExpectedLogData([]int{}) 98 | expected := fmt.Sprintln([]any{}...) 99 | require.Equal(t, expected, result) 100 | }) 101 | 102 | t.Run("empty array", func(t *testing.T) { 103 | result := logger.ExpectedLogData([...]int{}) 104 | expected := fmt.Sprintln([]any{}...) 105 | require.Equal(t, expected, result) 106 | }) 107 | } 108 | 109 | // errorWriter is an io.Writer implementation that always returns an error. 110 | type errorWriter struct{} 111 | 112 | func (e *errorWriter) Write(_ []byte) (int, error) { 113 | return 0, fmt.Errorf("error") 114 | } 115 | -------------------------------------------------------------------------------- /internal/testing/require/call.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package require 18 | 19 | import ( 20 | "errors" 21 | 22 | "github.com/unpleasantcam/componego/internal/testing" 23 | "github.com/unpleasantcam/componego/libs/vendor-proxy" 24 | ) 25 | 26 | func call(name string, t testing.T, args ...any) { 27 | if h, ok := t.(testing.THelper); ok { 28 | h.Helper() // Removes this function from the error stack. 29 | } 30 | var err error 31 | require := vendor_proxy.Get("testify/require") 32 | if len(args) == 0 { 33 | _, err = require.CallFunction(name, t) 34 | } else { 35 | newArgs := make([]any, 0, len(args)+1) 36 | newArgs = append(newArgs, t) 37 | newArgs = append(newArgs, args...) 38 | _, err = require.CallFunction(name, newArgs...) 39 | } 40 | if err == nil { 41 | return 42 | } 43 | message := "error: " + err.Error() 44 | if errors.Is(err, vendor_proxy.ErrFunctionNotExists) { 45 | message += messageOfIncorrectRun() 46 | } 47 | t.Errorf(message) 48 | t.FailNow() 49 | } 50 | 51 | func messageOfIncorrectRun() string { 52 | // noinspection SpellCheckingInspection 53 | return ` 54 | Make sure you run tests using 'make tests' or 'make tests-cover'. 55 | However, if you are running tests in your application, then you need to add a new imports: 56 | 57 | import ( 58 | _ "github.com/componego/meta-package/pre-init/vendor-proxy/for-app" 59 | _ "github.com/componego/meta-package/pre-init/vendor-proxy/for-tests" 60 | ) 61 | 62 | ` 63 | } 64 | -------------------------------------------------------------------------------- /internal/testing/require/tests/call_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/unpleasantcam/componego/internal/testing/require" 23 | ) 24 | 25 | func TestCallRequire(t *testing.T) { 26 | flag := true 27 | require.Panics(t, func() { 28 | flag = false 29 | panic("catch me if you can") 30 | }) 31 | require.False(t, flag) 32 | if flag { 33 | t.Fatal("flag is true") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /internal/testing/testing.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package testing 18 | 19 | type T interface { 20 | Errorf(format string, args ...any) 21 | FailNow() 22 | Cleanup(fn func()) 23 | } 24 | 25 | type THelper interface { 26 | T 27 | Helper() 28 | } 29 | 30 | type TParallel interface { 31 | T 32 | Parallel() 33 | } 34 | 35 | type TRun[I T] interface { 36 | T 37 | Run(name string, fn func(t I)) bool 38 | } 39 | -------------------------------------------------------------------------------- /internal/testing/types/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | // These are the internal types that are needed to test the framework. 20 | 21 | type CustomString string 22 | 23 | type AInterface interface { 24 | Method() 25 | } 26 | 27 | type AStruct struct { 28 | Value int 29 | } 30 | 31 | func (a *AStruct) Method() {} 32 | 33 | type BStruct struct { 34 | AStruct *AStruct 35 | } 36 | 37 | type CStruct struct { 38 | *AStruct `componego:"inject"` 39 | Value int 40 | privateField AInterface `componego:"inject"` 41 | PublicField1 *AStruct `componego:"inject,otherValue"` 42 | PublicField2 *BStruct 43 | IncorrectTag1 *AStruct `componego:"INJECT"` 44 | IncorrectTag2 *AStruct `COMPONEGO:"inject"` 45 | } 46 | 47 | type DStruct struct { 48 | PublicField2 AInterface `componego:"inject"` 49 | PublicField1 *CStruct `componego:"inject"` 50 | } 51 | 52 | func (c *CStruct) GetPrivateField() AInterface { 53 | return c.privateField 54 | } 55 | 56 | var _ AInterface = (*AStruct)(nil) 57 | -------------------------------------------------------------------------------- /internal/utils/context.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package utils 18 | 19 | import ( 20 | "context" 21 | "reflect" 22 | ) 23 | 24 | const parentContextField = "Context" 25 | 26 | func init() { 27 | checkContextForUsedGoVersion() 28 | } 29 | 30 | func IsParentContext(parent context.Context, child context.Context) (ok bool) { 31 | if parent == nil || child == nil { 32 | return false 33 | } 34 | for { 35 | if parent == child { 36 | return true 37 | } 38 | reflectValue := reflect.Indirect(reflect.ValueOf(child)) 39 | if reflectValue.Kind() != reflect.Struct { 40 | return false 41 | } 42 | contextField := reflectValue.FieldByName(parentContextField) 43 | if !contextField.IsValid() || contextField.IsNil() { 44 | return false 45 | } 46 | child, ok = contextField.Interface().(context.Context) 47 | if !ok { 48 | return false 49 | } 50 | } 51 | } 52 | 53 | func checkContextForUsedGoVersion() { 54 | errMessage := ` 55 | It looks like you are using a GoLang version not supported by the framework version installed in your project. 56 | This incompatibility was not detected during project compilation. 57 | --- 58 | You need to update your framework to the latest version. 59 | If this does not solve the problem, please create an issue on GitHub -> https://github.com/unpleasantcam/componego/issues 60 | We will fix this problem as soon as possible. 61 | ` 62 | parentCtx := context.Background() 63 | childCtx, cancel := context.WithCancel(parentCtx) 64 | defer cancel() 65 | reflectValue := reflect.Indirect(reflect.ValueOf(childCtx)) 66 | if reflectValue.Kind() != reflect.Struct { 67 | panic(errMessage) 68 | } 69 | contextField := reflectValue.FieldByName(parentContextField) 70 | if !contextField.IsValid() { 71 | panic(errMessage) 72 | } 73 | internalCtx, ok := contextField.Interface().(context.Context) 74 | if !ok || internalCtx != parentCtx { 75 | panic(errMessage) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /internal/utils/fprint.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package utils 18 | 19 | import ( 20 | "fmt" 21 | "io" 22 | ) 23 | 24 | const Indent = " " 25 | 26 | // noinspection SpellCheckingInspection 27 | func Fprint(writer io.Writer, args ...any) { 28 | _, err := fmt.Fprint(writer, args...) 29 | if err != nil { 30 | panic(err) 31 | } 32 | } 33 | 34 | // noinspection SpellCheckingInspection 35 | func Fprintln(writer io.Writer, args ...any) { 36 | // fmt.Fprintln adds unnecessary spaces between arguments. 37 | // This method brings the output to a single format without spaces. 38 | if len(args) > 0 { 39 | Fprint(writer, args...) 40 | } 41 | Fprint(writer, "\n") 42 | } 43 | 44 | func Fprintf(writer io.Writer, format string, args ...any) { 45 | _, err := fmt.Fprintf(writer, format, args...) 46 | if err != nil { 47 | panic(err) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/utils/reflect.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package utils 18 | 19 | import ( 20 | "reflect" 21 | ) 22 | 23 | var errorType = reflect.TypeOf((*error)(nil)).Elem() 24 | 25 | // IsErrorType returns true if the type is an error. 26 | func IsErrorType(reflectType reflect.Type) bool { 27 | return reflectType.Implements(errorType) 28 | } 29 | 30 | func Indirect(instance any) any { 31 | if instance == nil { 32 | return nil 33 | } 34 | if reflectType := reflect.TypeOf(instance); reflectType.Kind() != reflect.Pointer { 35 | return instance 36 | } 37 | reflectValue := reflect.ValueOf(instance) 38 | for reflectValue.Kind() == reflect.Pointer && !reflectValue.IsNil() { 39 | reflectValue = reflectValue.Elem() 40 | } 41 | return reflectValue.Interface() 42 | } 43 | 44 | func IsEmpty(instance any) bool { 45 | if instance == nil { 46 | return true 47 | } 48 | reflectValue := reflect.ValueOf(instance) 49 | switch reflectValue.Kind() { 50 | case reflect.Chan, reflect.Map, reflect.Slice: 51 | return reflectValue.Len() == 0 52 | case reflect.Pointer: 53 | if reflectValue.IsNil() { 54 | return true 55 | } 56 | return IsEmpty(reflectValue.Elem().Interface()) 57 | } 58 | return reflect.DeepEqual(instance, reflect.Zero(reflectValue.Type()).Interface()) 59 | } 60 | -------------------------------------------------------------------------------- /internal/utils/slice.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package utils 18 | 19 | func Keys[K comparable, V any](items map[K]V) []K { 20 | result := make([]K, 0, len(items)) 21 | for key := range items { 22 | result = append(result, key) 23 | } 24 | return result 25 | } 26 | 27 | func Values[K comparable, V any](items map[K]V) []V { 28 | result := make([]V, 0, len(items)) 29 | for _, item := range items { 30 | result = append(result, item) 31 | } 32 | return result 33 | } 34 | 35 | func Contains[T comparable](items []T, value T) bool { 36 | for _, item := range items { 37 | if value == item { 38 | return true 39 | } 40 | } 41 | return false 42 | } 43 | 44 | func Reverse[T any](items []T) { 45 | for i, j := 0, len(items)-1; i < j; i, j = i+1, j-1 { 46 | items[i], items[j] = items[j], items[i] 47 | } 48 | } 49 | 50 | func Copy[T any](items []T) []T { 51 | result := make([]T, len(items)) 52 | copy(result, items) 53 | return result 54 | } 55 | -------------------------------------------------------------------------------- /internal/utils/tests/context_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "context" 21 | "testing" 22 | "time" 23 | 24 | "github.com/unpleasantcam/componego/internal/utils" 25 | ) 26 | 27 | func TestIsParentContext(t *testing.T) { 28 | parentCtx := context.Background() 29 | childCtx1, cancel1 := context.WithCancel(parentCtx) 30 | defer cancel1() 31 | childCtx2, cancel2 := context.WithTimeout(childCtx1, time.Minute*60) 32 | defer cancel2() 33 | childCtx3, cancel3 := context.WithDeadline(childCtx1, time.Now().Add(time.Minute*60)) 34 | defer cancel3() 35 | testCases := [...]struct { 36 | a context.Context 37 | b context.Context 38 | status bool 39 | }{ 40 | {parentCtx, childCtx1, true}, // 1 41 | {childCtx1, parentCtx, false}, // 2 42 | // 43 | {parentCtx, childCtx2, true}, // 3 44 | {childCtx2, parentCtx, false}, // 4 45 | // 46 | {childCtx1, childCtx2, true}, // 5 47 | {childCtx2, childCtx1, false}, // 6 48 | // 49 | {childCtx1, childCtx3, true}, // 7 50 | {childCtx3, childCtx1, false}, // 8 51 | // 52 | {parentCtx, context.Background(), true}, // 9 53 | {context.Background(), context.Background(), true}, // 10 54 | {context.Background(), parentCtx, true}, // 11 55 | {context.Background(), childCtx1, true}, // 12 56 | {context.Background(), context.TODO(), false}, // 13 57 | {context.TODO(), context.Background(), false}, // 14 58 | } 59 | for i, testCase := range testCases { 60 | if utils.IsParentContext(testCase.a, testCase.b) != testCase.status { 61 | message := "Failed to check parent context (#%d). Make sure the framework is compatible with your version of Go." 62 | t.Fatalf(message, i+1) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /internal/utils/tests/fprint_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "bytes" 21 | "errors" 22 | "fmt" 23 | "io" 24 | "testing" 25 | 26 | "github.com/unpleasantcam/componego/internal/testing/require" 27 | "github.com/unpleasantcam/componego/internal/utils" 28 | ) 29 | 30 | // noinspection SpellCheckingInspection 31 | func TestFprint(t *testing.T) { 32 | t.Run("single string", func(t *testing.T) { 33 | var buffer bytes.Buffer 34 | utils.Fprint(&buffer, "hello") 35 | require.Equal(t, "hello", buffer.String()) 36 | }) 37 | 38 | t.Run("different input types", func(t *testing.T) { 39 | var buffer bytes.Buffer 40 | utils.Fprint(&buffer, "hello", 123, " ", '1') 41 | require.Equal(t, "hello123 49", buffer.String()) 42 | }) 43 | 44 | t.Run("empty input", func(t *testing.T) { 45 | var buffer bytes.Buffer 46 | utils.Fprint(&buffer) 47 | require.Equal(t, buffer.Len(), 0) 48 | }) 49 | 50 | t.Run("error case", func(t *testing.T) { 51 | require.Panics(t, func() { 52 | utils.Fprint(&errorWriter{}, "this will fail") 53 | }) 54 | }) 55 | } 56 | 57 | // noinspection SpellCheckingInspection 58 | func TestFprintln(t *testing.T) { 59 | t.Run("single string", func(t *testing.T) { 60 | var buffer bytes.Buffer 61 | utils.Fprintln(&buffer, "hello") 62 | require.Equal(t, fmt.Sprintln("hello"), buffer.String()) 63 | }) 64 | 65 | t.Run("different input types", func(t *testing.T) { 66 | var buffer bytes.Buffer 67 | utils.Fprintln(&buffer, "hello", 123, " ", '1') 68 | require.Equal(t, fmt.Sprintln("hello123 49"), buffer.String()) 69 | }) 70 | 71 | t.Run("empty input", func(t *testing.T) { 72 | var buffer bytes.Buffer 73 | utils.Fprintln(&buffer) 74 | require.Equal(t, fmt.Sprintln(), buffer.String()) 75 | }) 76 | 77 | t.Run("error case", func(t *testing.T) { 78 | require.Panics(t, func() { 79 | utils.Fprintln(&errorWriter{}, "this will fail") 80 | }) 81 | }) 82 | } 83 | 84 | func TestFprintf(t *testing.T) { 85 | t.Run("simple formatting", func(t *testing.T) { 86 | var buffer bytes.Buffer 87 | utils.Fprintf(&buffer, "hello %s", "world") 88 | require.Equal(t, "hello world", buffer.String()) 89 | }) 90 | 91 | t.Run("multiple format specifiers", func(t *testing.T) { 92 | var buffer bytes.Buffer 93 | utils.Fprintf(&buffer, "integer: %d, string: %s", 123, "test") 94 | require.Equal(t, "integer: 123, string: test", buffer.String()) 95 | }) 96 | 97 | t.Run("empty format string", func(t *testing.T) { 98 | var buffer bytes.Buffer 99 | utils.Fprintf(&buffer, "") 100 | require.Equal(t, buffer.Len(), 0) 101 | }) 102 | 103 | t.Run("error case", func(t *testing.T) { 104 | require.Panics(t, func() { 105 | utils.Fprintf(&errorWriter{}, "this will fail") 106 | }) 107 | }) 108 | } 109 | 110 | // Custom io.Writer that always returns an error. 111 | type errorWriter struct{} 112 | 113 | func (e *errorWriter) Write(_ []byte) (n int, err error) { 114 | return 0, errors.New("write error") 115 | } 116 | 117 | var _ io.Writer = (*errorWriter)(nil) 118 | -------------------------------------------------------------------------------- /libs/color/color.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package color 18 | 19 | import ( 20 | "bytes" 21 | "io" 22 | "strconv" 23 | ) 24 | 25 | type Color int 26 | 27 | const ( 28 | BlackColor Color = 30 29 | BlueColor Color = 34 30 | CyanColor Color = 36 31 | GrayColor Color = 37 32 | GreenColor Color = 32 33 | PurpleColor Color = 35 34 | RedColor Color = 31 35 | WhiteColor Color = 97 36 | YellowColor Color = 33 37 | 38 | BlackBackground Color = 40 39 | BlueBackground Color = 44 40 | CyanBackground Color = 46 41 | GrayBackground Color = 47 42 | GreenBackground Color = 42 43 | PurpleBackground Color = 45 44 | RedBackground Color = 41 45 | WhiteBackground Color = 107 46 | YellowBackground Color = 43 47 | 48 | BoldText Color = 1 49 | UnderlineText Color = 4 50 | 51 | reset Color = 0 52 | ) 53 | 54 | var _active = false // Colors are disabled by default 55 | 56 | func SetIsActive(active bool) { 57 | _active = active 58 | } 59 | 60 | func GetIsActive() bool { 61 | return _active 62 | } 63 | 64 | type coloredWriter struct { 65 | writer io.Writer 66 | colors []Color 67 | } 68 | 69 | func NewColoredWriter(writer io.Writer, colors ...Color) io.Writer { 70 | return &coloredWriter{ 71 | writer: writer, 72 | colors: colors, 73 | } 74 | } 75 | 76 | func (s *coloredWriter) Write(data []byte) (int, error) { 77 | if !_active || len(s.colors) == 0 { 78 | return s.writer.Write(data) 79 | } 80 | data = normalize(data) 81 | if bytes.IndexByte(data, '\n') < 0 { 82 | newData := make([]byte, 0, len(data)+(len(s.colors)+1)*5) // 5 is the approximate size of one color in bytes. 83 | newData = appendColors(newData, data, s.colors) 84 | return s.writer.Write(newData) 85 | } 86 | // In some cases, colors may color a new line if there is a new line. 87 | // Colors will not be applied to the new line because it will look ugly. 88 | items := bytes.Split(data, []byte{'\n'}) 89 | newData := make([]byte, 0, len(data)+(len(s.colors)+1)*len(items)*5) // 5 is the approximate size of one color in bytes. 90 | for i, data := range items { 91 | if i > 0 { 92 | newData = append(newData, '\n') 93 | } 94 | newData = appendColors(newData, data, s.colors) 95 | } 96 | return s.writer.Write(newData) 97 | } 98 | 99 | func normalize(data []byte) []byte { 100 | if bytes.IndexByte(data, '\r') < 0 { 101 | return data 102 | } 103 | data = bytes.ReplaceAll(data, []byte("\r\n"), []byte{'\n'}) 104 | if bytes.IndexByte(data, '\r') < 0 { 105 | return data 106 | } 107 | return bytes.ReplaceAll(data, []byte{'\r'}, []byte{'\n'}) 108 | } 109 | 110 | func appendColors(newData, data []byte, colors []Color) []byte { 111 | if len(data) == 0 { 112 | return newData 113 | } 114 | for _, color := range colors { 115 | newData = colorToBytes(newData, color) 116 | } 117 | newData = append(newData, data...) 118 | return colorToBytes(newData, reset) 119 | } 120 | 121 | func colorToBytes(data []byte, color Color) []byte { 122 | data = append(data, "\033["...) 123 | data = append(data, []byte(strconv.Itoa(int(color)))...) 124 | data = append(data, 'm') 125 | return data 126 | } 127 | -------------------------------------------------------------------------------- /libs/color/theme.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package color 18 | 19 | import ( 20 | "fmt" 21 | "io" 22 | ) 23 | 24 | var themeFactories map[string]themeFactory 25 | 26 | func init() { 27 | themeFactories = make(map[string]themeFactory, 5) 28 | } 29 | 30 | type themeFactory = func(writer io.Writer) Theme 31 | 32 | type Theme interface { 33 | GetWriter(name string) io.Writer 34 | } 35 | 36 | type theme struct { 37 | defaultWriter io.Writer 38 | colors map[string]io.Writer 39 | } 40 | 41 | func NewTheme(writer io.Writer, colors map[string][]Color) Theme { 42 | theme := &theme{ 43 | defaultWriter: writer, 44 | colors: make(map[string]io.Writer, len(colors)), 45 | } 46 | for name, colors := range colors { 47 | theme.colors[name] = NewColoredWriter(writer, colors...) 48 | } 49 | return theme 50 | } 51 | 52 | func (t *theme) GetWriter(name string) io.Writer { 53 | if writer, ok := t.colors[name]; ok { 54 | return writer 55 | } 56 | return t.defaultWriter 57 | } 58 | 59 | func AddTheme(name string, themeFactory themeFactory) { 60 | themeFactories[name] = themeFactory 61 | } 62 | 63 | func GetTheme(name string, writer io.Writer) Theme { 64 | if factory, ok := themeFactories[name]; ok { 65 | return factory(writer) 66 | } 67 | // We use panic here because we don't want you to handle an error when the theme doesn't exist. 68 | // The theme should always exist if you use it. 69 | panic(fmt.Sprintf("theme '%s' not found", name)) 70 | } 71 | 72 | func HasTheme(name string) bool { 73 | _, ok := themeFactories[name] 74 | return ok 75 | } 76 | -------------------------------------------------------------------------------- /libs/debug/stack.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package debug 18 | 19 | import ( 20 | "fmt" 21 | "runtime" 22 | "strings" 23 | ) 24 | 25 | type StackTrace []uintptr 26 | 27 | func GetStackTrace(skip int) *StackTrace { 28 | const depth = 32 29 | var pcs [depth]uintptr 30 | position := runtime.Callers(2+skip, pcs[:]) 31 | var stackTrace StackTrace = pcs[0:position] 32 | return &stackTrace 33 | } 34 | 35 | func (s *StackTrace) String() string { 36 | var builder strings.Builder 37 | for _, pc := range *s { 38 | frame := StackFrame(pc) 39 | builder.WriteString(frame.String()) 40 | builder.WriteString("\n") 41 | } 42 | return builder.String() 43 | } 44 | 45 | type StackFrame uintptr 46 | 47 | func (s StackFrame) PC() uintptr { 48 | return uintptr(s) - 1 49 | } 50 | 51 | func (s StackFrame) FileLine() (string, int) { 52 | fn := runtime.FuncForPC(s.PC()) 53 | if fn == nil { 54 | return "unknown", 0 55 | } 56 | return fn.FileLine(s.PC()) 57 | } 58 | 59 | func (s StackFrame) Name() string { 60 | fn := runtime.FuncForPC(s.PC()) 61 | if fn == nil { 62 | return "unknown" 63 | } 64 | return fn.Name() 65 | } 66 | 67 | func (s StackFrame) String() string { 68 | file, line := s.FileLine() 69 | return fmt.Sprintf("%s: %s:%d", s.Name(), file, line) 70 | } 71 | 72 | var ( 73 | _ fmt.Stringer = (*StackTrace)(nil) 74 | _ fmt.Stringer = (*StackFrame)(nil) 75 | ) 76 | -------------------------------------------------------------------------------- /libs/debug/tests/variable_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "reflect" 21 | "strings" 22 | "testing" 23 | 24 | "github.com/unpleasantcam/componego/internal/testing/require" 25 | "github.com/unpleasantcam/componego/libs/debug" 26 | ) 27 | 28 | type testStruct1 struct { 29 | A float64 `json:"a"` 30 | B []any 31 | c bool 32 | D *testStruct2 33 | } 34 | 35 | type testStruct2 struct { 36 | A bool 37 | B []string 38 | } 39 | 40 | func TestRenderVariable(t *testing.T) { 41 | require.Equal(t, "nil", debug.RenderVariable(nil, nil)) 42 | require.Equal(t, "int{ 1 }", debug.RenderVariable(1, nil)) 43 | require.Equal(t, `string{ "1" }`, debug.RenderVariable("1", nil)) 44 | require.Equal(t, "int", debug.RenderVariable(reflect.TypeOf(1), nil)) 45 | require.Equal(t, "int", debug.RenderVariable(reflect.ValueOf(1), nil)) 46 | require.Regexp(t, `func \S+\(float64, \.\.\.string\) error`, debug.RenderVariable(func(_ float64, _ ...string) error { return nil }, nil)) 47 | require.Equal(t, "struct {}{[... no public data]}", debug.RenderVariable(struct{}{}, nil)) 48 | structObj := &testStruct1{ 49 | A: 0.1, 50 | B: []any{"1", 2, false}, 51 | c: true, 52 | D: &testStruct2{ 53 | A: false, 54 | B: []string{"text"}, 55 | }, 56 | } 57 | structOutput := `*tests.testStruct1{ "a": 0.1, "B": { 0: "1", 1: 2, 2: false, }, "D": { "A": false, "B": []string{[... read depth exceeded]}, }, }` 58 | require.Equal(t, structOutput, debug.RenderVariable(structObj, nil)) 59 | structOutput = `*tests.testStruct1{ 60 | "a": 0.1, 61 | "B": { 62 | 0: "1", 63 | 1: 2, 64 | 2: false, 65 | }, 66 | "D": { 67 | "A": false, 68 | "B": []string{[... read depth exceeded]}, 69 | }, 70 | }` 71 | require.Equal(t, structOutput, debug.RenderVariable(structObj, &debug.VariableConfig{ 72 | Indent: " ", 73 | UseNewLine: true, 74 | MaxDepth: 3, 75 | MaxItems: 10, 76 | })) 77 | structOutput = strings.ReplaceAll(strings.ReplaceAll(structOutput, " ", "@@"), "\n", " ") 78 | require.Equal(t, structOutput, debug.RenderVariable(structObj, &debug.VariableConfig{ 79 | Indent: "@@", 80 | UseNewLine: false, 81 | MaxDepth: 3, 82 | MaxItems: 10, 83 | })) 84 | structOutput = `*tests.testStruct1{ 85 | --"a": 0.1, 86 | --"B": []any{[... read depth exceeded]}, 87 | --"D": *tests.testStruct2{[... read depth exceeded]}, 88 | }` 89 | require.Equal(t, structOutput, debug.RenderVariable(structObj, &debug.VariableConfig{ 90 | Indent: "--", 91 | UseNewLine: true, 92 | MaxDepth: 2, 93 | MaxItems: 2, 94 | })) 95 | structOutput = `*tests.testStruct1{ 96 | "a": 0.1, 97 | "B": { 98 | 0: "1", 99 | [...and other 2 items], 100 | }, 101 | "D": { 102 | "A": false, 103 | "B": { 104 | 0: "text", 105 | }, 106 | }, 107 | }` 108 | require.Equal(t, structOutput, debug.RenderVariable(structObj, &debug.VariableConfig{ 109 | Indent: " ", 110 | UseNewLine: true, 111 | MaxDepth: 4, 112 | MaxItems: 1, 113 | })) 114 | } 115 | -------------------------------------------------------------------------------- /libs/ordered-map/tests/map_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/unpleasantcam/componego/libs/ordered-map" 23 | ) 24 | 25 | func TestOrderedMap(t *testing.T) { 26 | MapTester[*testing.T](t, ordered_map.New[int, float64]) 27 | } 28 | 29 | func BenchmarkOrderedMap(b *testing.B) { 30 | b.Run("set", func(b *testing.B) { 31 | m := ordered_map.New[int, int](b.N) 32 | b.ReportAllocs() 33 | for n := 0; n < b.N; n++ { 34 | m.Set(n, n) 35 | } 36 | }) 37 | b.Run("get", func(b *testing.B) { 38 | m := ordered_map.New[int, int](1) 39 | m.Set(0, 0) 40 | b.ReportAllocs() 41 | for n := 0; n < b.N; n++ { 42 | m.Get(0) 43 | } 44 | }) 45 | b.Run("has", func(b *testing.B) { 46 | m := ordered_map.New[int, int](1) 47 | m.Set(0, 0) 48 | b.ReportAllocs() 49 | for n := 0; n < b.N; n++ { 50 | m.Has(0) 51 | } 52 | }) 53 | b.Run("prepend", func(b *testing.B) { 54 | m := ordered_map.New[int, int](b.N) 55 | b.ReportAllocs() 56 | for n := 0; n < b.N; n++ { 57 | m.Prepend(n, n) 58 | } 59 | }) 60 | b.Run("append", func(b *testing.B) { 61 | m := ordered_map.New[int, int](b.N) 62 | b.ReportAllocs() 63 | for n := 0; n < b.N; n++ { 64 | m.Append(n, n) 65 | } 66 | }) 67 | b.Run("add before", func(b *testing.B) { 68 | m := ordered_map.New[int, int](b.N) 69 | b.ReportAllocs() 70 | for n := 0; n < b.N; n++ { 71 | m.AddBefore(n, n, n-1) 72 | } 73 | }) 74 | b.Run("add after", func(b *testing.B) { 75 | m := ordered_map.New[int, int](b.N) 76 | b.ReportAllocs() 77 | for n := 0; n < b.N; n++ { 78 | m.AddAfter(n, n, n-1) 79 | } 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /libs/type-cast/bool.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package type_cast 18 | 19 | import ( 20 | "fmt" 21 | "strconv" 22 | 23 | "github.com/unpleasantcam/componego/internal/utils" 24 | ) 25 | 26 | func ToBool(value any) (bool, error) { 27 | value = utils.Indirect(value) 28 | switch castedValue := value.(type) { 29 | case nil: 30 | return false, nil 31 | case bool: 32 | return castedValue, nil 33 | case int: 34 | return castedValue != 0, nil 35 | case int8: 36 | return castedValue != 0, nil 37 | case int16: 38 | return castedValue != 0, nil 39 | case int32: 40 | return castedValue != 0, nil 41 | case int64: 42 | return castedValue != 0, nil 43 | case uint: 44 | return castedValue != 0, nil 45 | case uint8: 46 | return castedValue != 0, nil 47 | case uint16: 48 | return castedValue != 0, nil 49 | case uint32: 50 | return castedValue != 0, nil 51 | case uint64: 52 | return castedValue != 0, nil 53 | case float32: 54 | return castedValue != 0, nil 55 | case float64: 56 | return castedValue != 0, nil 57 | case string: 58 | return strconv.ParseBool(castedValue) 59 | } 60 | return false, fmt.Errorf("unable to cast %#v of type %T to bool", value, value) 61 | } 62 | -------------------------------------------------------------------------------- /libs/type-cast/float.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package type_cast 18 | 19 | import ( 20 | "fmt" 21 | "strconv" 22 | 23 | "github.com/unpleasantcam/componego/internal/utils" 24 | ) 25 | 26 | func ToFloat64(value any) (float64, error) { 27 | value = utils.Indirect(value) 28 | switch castedValue := value.(type) { 29 | case nil: 30 | return 0, nil 31 | case bool: 32 | if castedValue { 33 | return 1, nil 34 | } 35 | return 0, nil 36 | case int: 37 | return float64(castedValue), nil 38 | case int8: 39 | return float64(castedValue), nil 40 | case int16: 41 | return float64(castedValue), nil 42 | case int32: 43 | return float64(castedValue), nil 44 | case int64: 45 | return float64(castedValue), nil 46 | case uint: 47 | return float64(castedValue), nil 48 | case uint8: 49 | return float64(castedValue), nil 50 | case uint16: 51 | return float64(castedValue), nil 52 | case uint32: 53 | return float64(castedValue), nil 54 | case uint64: 55 | return float64(castedValue), nil 56 | case float32: 57 | return float64(castedValue), nil 58 | case float64: 59 | return castedValue, nil 60 | case string: 61 | return strconv.ParseFloat(castedValue, 64) 62 | } 63 | return 0, fmt.Errorf("unable to cast %#v of type %T to float64", value, value) 64 | } 65 | -------------------------------------------------------------------------------- /libs/type-cast/int.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package type_cast 18 | 19 | import ( 20 | "fmt" 21 | "math" 22 | "strconv" 23 | 24 | "github.com/unpleasantcam/componego/internal/utils" 25 | ) 26 | 27 | func ToInt64(value any) (int64, error) { 28 | value = utils.Indirect(value) 29 | switch castedValue := value.(type) { 30 | case nil: 31 | return 0, nil 32 | case bool: 33 | if castedValue { 34 | return 1, nil 35 | } 36 | return 0, nil 37 | case int: 38 | return int64(castedValue), nil 39 | case int8: 40 | return int64(castedValue), nil 41 | case int16: 42 | return int64(castedValue), nil 43 | case int32: 44 | return int64(castedValue), nil 45 | case int64: 46 | return castedValue, nil 47 | case uint: 48 | if castedValue > math.MaxInt64 { 49 | return 0, fmt.Errorf("unable to cast %#v of type %T to int64", value, value) 50 | } 51 | return int64(castedValue), nil 52 | case uint8: 53 | return int64(castedValue), nil 54 | case uint16: 55 | return int64(castedValue), nil 56 | case uint32: 57 | return int64(castedValue), nil 58 | case uint64: 59 | if castedValue > math.MaxInt64 { 60 | return 0, fmt.Errorf("unable to cast %#v of type %T to int64", value, value) 61 | } 62 | return int64(castedValue), nil 63 | case float32: 64 | return int64(castedValue), nil 65 | case float64: 66 | return int64(castedValue), nil 67 | case string: 68 | return strconv.ParseInt(castedValue, 0, 0) 69 | } 70 | return 0, fmt.Errorf("unable to cast %#v of type %T to int64", value, value) 71 | } 72 | -------------------------------------------------------------------------------- /libs/type-cast/string.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package type_cast 18 | 19 | import ( 20 | "fmt" 21 | "strconv" 22 | 23 | "github.com/unpleasantcam/componego/internal/utils" 24 | ) 25 | 26 | func ToString(value any) (string, error) { 27 | value = utils.Indirect(value) 28 | switch castedValue := value.(type) { 29 | case nil: 30 | return "", nil 31 | case bool: 32 | return strconv.FormatBool(castedValue), nil 33 | case int: 34 | return strconv.Itoa(castedValue), nil 35 | case int8: 36 | return strconv.FormatInt(int64(castedValue), 10), nil 37 | case int16: 38 | return strconv.FormatInt(int64(castedValue), 10), nil 39 | case int32: 40 | return strconv.FormatInt(int64(castedValue), 10), nil 41 | case int64: 42 | return strconv.FormatInt(castedValue, 10), nil 43 | case uint: 44 | return strconv.FormatUint(uint64(castedValue), 10), nil 45 | case uint8: 46 | return strconv.FormatInt(int64(castedValue), 10), nil 47 | case uint16: 48 | return strconv.FormatInt(int64(castedValue), 10), nil 49 | case uint32: 50 | return strconv.FormatInt(int64(castedValue), 10), nil 51 | case uint64: 52 | return strconv.FormatUint(castedValue, 10), nil 53 | case float32: 54 | return strconv.FormatFloat(float64(castedValue), 'f', -1, 32), nil 55 | case float64: 56 | return strconv.FormatFloat(castedValue, 'f', -1, 64), nil 57 | case string: 58 | return castedValue, nil 59 | } 60 | return "", fmt.Errorf("unable to cast %#v of type %T to string", value, value) 61 | } 62 | -------------------------------------------------------------------------------- /libs/type-cast/tests/float_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/unpleasantcam/componego/internal/testing/require" 23 | "github.com/unpleasantcam/componego/libs/type-cast" 24 | ) 25 | 26 | func TestToFloat64(t *testing.T) { 27 | t.Run("nil value", func(t *testing.T) { 28 | result, err := type_cast.ToFloat64(nil) 29 | require.NoError(t, err) 30 | require.Equal(t, float64(0), result) 31 | }) 32 | 33 | t.Run("bool value true", func(t *testing.T) { 34 | result, err := type_cast.ToFloat64(true) 35 | require.NoError(t, err) 36 | require.Equal(t, float64(1), result) 37 | }) 38 | 39 | t.Run("bool value false", func(t *testing.T) { 40 | result, err := type_cast.ToFloat64(false) 41 | require.NoError(t, err) 42 | require.Equal(t, float64(0), result) 43 | }) 44 | 45 | t.Run("int value", func(t *testing.T) { 46 | result, err := type_cast.ToFloat64(123) 47 | require.NoError(t, err) 48 | require.Equal(t, float64(123), result) 49 | }) 50 | 51 | t.Run("int8 value", func(t *testing.T) { 52 | result, err := type_cast.ToFloat64(int8(123)) 53 | require.NoError(t, err) 54 | require.Equal(t, float64(123), result) 55 | }) 56 | 57 | t.Run("int16 value", func(t *testing.T) { 58 | result, err := type_cast.ToFloat64(int16(123)) 59 | require.NoError(t, err) 60 | require.Equal(t, float64(123), result) 61 | }) 62 | 63 | t.Run("int32 value", func(t *testing.T) { 64 | result, err := type_cast.ToFloat64(int32(123)) 65 | require.NoError(t, err) 66 | require.Equal(t, float64(123), result) 67 | }) 68 | 69 | t.Run("int64 value", func(t *testing.T) { 70 | result, err := type_cast.ToFloat64(int64(123)) 71 | require.NoError(t, err) 72 | require.Equal(t, float64(123), result) 73 | }) 74 | 75 | t.Run("uint value", func(t *testing.T) { 76 | result, err := type_cast.ToFloat64(uint(123)) 77 | require.NoError(t, err) 78 | require.Equal(t, float64(123), result) 79 | }) 80 | 81 | t.Run("uint8 value", func(t *testing.T) { 82 | result, err := type_cast.ToFloat64(uint8(123)) 83 | require.NoError(t, err) 84 | require.Equal(t, float64(123), result) 85 | }) 86 | 87 | t.Run("uint16 value", func(t *testing.T) { 88 | result, err := type_cast.ToFloat64(uint16(123)) 89 | require.NoError(t, err) 90 | require.Equal(t, float64(123), result) 91 | }) 92 | 93 | t.Run("uint32 value", func(t *testing.T) { 94 | result, err := type_cast.ToFloat64(uint32(123)) 95 | require.NoError(t, err) 96 | require.Equal(t, float64(123), result) 97 | }) 98 | 99 | t.Run("uint64 value", func(t *testing.T) { 100 | result, err := type_cast.ToFloat64(uint64(123)) 101 | require.NoError(t, err) 102 | require.Equal(t, float64(123), result) 103 | }) 104 | 105 | t.Run("float32 value", func(t *testing.T) { 106 | result, err := type_cast.ToFloat64(float32(123.123)) 107 | require.NoError(t, err) 108 | // noinspection ALL 109 | require.InDelta(t, float64(123.123), result, 1e-5) 110 | }) 111 | 112 | t.Run("float64 value", func(t *testing.T) { 113 | // noinspection ALL 114 | result, err := type_cast.ToFloat64(float64(123.123)) 115 | require.NoError(t, err) 116 | // noinspection ALL 117 | require.Equal(t, float64(123.123), result) 118 | }) 119 | 120 | t.Run("string value", func(t *testing.T) { 121 | result, err := type_cast.ToFloat64("123.123") 122 | require.NoError(t, err) 123 | // noinspection ALL 124 | require.Equal(t, float64(123.123), result) 125 | }) 126 | 127 | t.Run("string value invalid", func(t *testing.T) { 128 | _, err := type_cast.ToFloat64("invalid") 129 | require.Error(t, err) 130 | }) 131 | 132 | t.Run("struct value invalid", func(t *testing.T) { 133 | _, err := type_cast.ToFloat64(struct{}{}) 134 | require.Error(t, err) 135 | }) 136 | } 137 | -------------------------------------------------------------------------------- /libs/type-cast/tests/int_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "math" 21 | "testing" 22 | 23 | "github.com/unpleasantcam/componego/internal/testing/require" 24 | "github.com/unpleasantcam/componego/libs/type-cast" 25 | ) 26 | 27 | func TestToInt64(t *testing.T) { 28 | t.Run("nil value", func(t *testing.T) { 29 | result, err := type_cast.ToInt64(nil) 30 | require.NoError(t, err) 31 | require.Equal(t, int64(0), result) 32 | }) 33 | 34 | t.Run("bool value true", func(t *testing.T) { 35 | result, err := type_cast.ToInt64(true) 36 | require.NoError(t, err) 37 | require.Equal(t, int64(1), result) 38 | }) 39 | 40 | t.Run("bool value false", func(t *testing.T) { 41 | result, err := type_cast.ToInt64(false) 42 | require.NoError(t, err) 43 | require.Equal(t, int64(0), result) 44 | }) 45 | 46 | t.Run("int value", func(t *testing.T) { 47 | result, err := type_cast.ToInt64(123) 48 | require.NoError(t, err) 49 | require.Equal(t, int64(123), result) 50 | }) 51 | 52 | t.Run("int8 value", func(t *testing.T) { 53 | result, err := type_cast.ToInt64(int8(123)) 54 | require.NoError(t, err) 55 | require.Equal(t, int64(123), result) 56 | }) 57 | 58 | t.Run("int16 value", func(t *testing.T) { 59 | result, err := type_cast.ToInt64(int16(123)) 60 | require.NoError(t, err) 61 | require.Equal(t, int64(123), result) 62 | }) 63 | 64 | t.Run("int32 value", func(t *testing.T) { 65 | result, err := type_cast.ToInt64(int32(123)) 66 | require.NoError(t, err) 67 | require.Equal(t, int64(123), result) 68 | }) 69 | 70 | t.Run("int64 value", func(t *testing.T) { 71 | result, err := type_cast.ToInt64(int64(123)) 72 | require.NoError(t, err) 73 | require.Equal(t, int64(123), result) 74 | }) 75 | 76 | t.Run("uint value", func(t *testing.T) { 77 | result, err := type_cast.ToInt64(uint(123)) 78 | require.NoError(t, err) 79 | require.Equal(t, int64(123), result) 80 | }) 81 | 82 | t.Run("uint8 value", func(t *testing.T) { 83 | result, err := type_cast.ToInt64(uint8(123)) 84 | require.NoError(t, err) 85 | require.Equal(t, int64(123), result) 86 | }) 87 | 88 | t.Run("uint16 value", func(t *testing.T) { 89 | result, err := type_cast.ToInt64(uint16(123)) 90 | require.NoError(t, err) 91 | require.Equal(t, int64(123), result) 92 | }) 93 | 94 | t.Run("uint32 value", func(t *testing.T) { 95 | result, err := type_cast.ToInt64(uint32(123)) 96 | require.NoError(t, err) 97 | require.Equal(t, int64(123), result) 98 | }) 99 | 100 | t.Run("uint64 value", func(t *testing.T) { 101 | result, err := type_cast.ToInt64(uint64(123)) 102 | require.NoError(t, err) 103 | require.Equal(t, int64(123), result) 104 | }) 105 | 106 | t.Run("float32 value", func(t *testing.T) { 107 | result, err := type_cast.ToInt64(float32(123.123)) 108 | require.NoError(t, err) 109 | require.Equal(t, int64(123), result) 110 | }) 111 | 112 | t.Run("float64 value", func(t *testing.T) { 113 | // noinspection ALL 114 | result, err := type_cast.ToInt64(float64(123.123)) 115 | require.NoError(t, err) 116 | require.Equal(t, int64(123), result) 117 | }) 118 | 119 | t.Run("string value", func(t *testing.T) { 120 | result, err := type_cast.ToInt64("123") 121 | require.NoError(t, err) 122 | require.Equal(t, int64(123), result) 123 | }) 124 | 125 | t.Run("string value invalid", func(t *testing.T) { 126 | _, err := type_cast.ToInt64("invalid") 127 | require.Error(t, err) 128 | }) 129 | 130 | t.Run("struct value invalid", func(t *testing.T) { 131 | _, err := type_cast.ToInt64(struct{}{}) 132 | require.Error(t, err) 133 | }) 134 | 135 | t.Run("uint input above MaxInt64", func(t *testing.T) { 136 | result, err := type_cast.ToInt64(uint(math.MaxInt64 + 1)) 137 | require.Error(t, err) 138 | require.Equal(t, int64(0), result) 139 | }) 140 | 141 | t.Run("uint64 input below MaxInt64", func(t *testing.T) { 142 | result, err := type_cast.ToInt64(uint64(math.MaxInt64 + 1)) 143 | require.Error(t, err) 144 | require.Equal(t, int64(0), result) 145 | }) 146 | } 147 | -------------------------------------------------------------------------------- /libs/type-cast/tests/string_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/unpleasantcam/componego/internal/testing/require" 23 | "github.com/unpleasantcam/componego/libs/type-cast" 24 | ) 25 | 26 | func TestToString(t *testing.T) { 27 | t.Run("nil value", func(t *testing.T) { 28 | result, err := type_cast.ToString(nil) 29 | require.NoError(t, err) 30 | require.Equal(t, "", result) 31 | }) 32 | 33 | t.Run("bool value", func(t *testing.T) { 34 | result, err := type_cast.ToString(true) 35 | require.NoError(t, err) 36 | require.Equal(t, "true", result) 37 | }) 38 | 39 | t.Run("int value", func(t *testing.T) { 40 | result, err := type_cast.ToString(123) 41 | require.NoError(t, err) 42 | require.Equal(t, "123", result) 43 | }) 44 | 45 | t.Run("int8 value", func(t *testing.T) { 46 | result, err := type_cast.ToString(int8(123)) 47 | require.NoError(t, err) 48 | require.Equal(t, "123", result) 49 | }) 50 | 51 | t.Run("int16 value", func(t *testing.T) { 52 | result, err := type_cast.ToString(int16(123)) 53 | require.NoError(t, err) 54 | require.Equal(t, "123", result) 55 | }) 56 | 57 | t.Run("int32 value", func(t *testing.T) { 58 | result, err := type_cast.ToString(int32(123)) 59 | require.NoError(t, err) 60 | require.Equal(t, "123", result) 61 | }) 62 | 63 | t.Run("int64 value", func(t *testing.T) { 64 | result, err := type_cast.ToString(int64(123)) 65 | require.NoError(t, err) 66 | require.Equal(t, "123", result) 67 | }) 68 | 69 | t.Run("uint value", func(t *testing.T) { 70 | result, err := type_cast.ToString(uint(123)) 71 | require.NoError(t, err) 72 | require.Equal(t, "123", result) 73 | }) 74 | 75 | t.Run("uint8 value", func(t *testing.T) { 76 | result, err := type_cast.ToString(uint8(123)) 77 | require.NoError(t, err) 78 | require.Equal(t, "123", result) 79 | }) 80 | 81 | t.Run("uint16 value", func(t *testing.T) { 82 | result, err := type_cast.ToString(uint16(123)) 83 | require.NoError(t, err) 84 | require.Equal(t, "123", result) 85 | }) 86 | 87 | t.Run("uint32 value", func(t *testing.T) { 88 | result, err := type_cast.ToString(uint32(123)) 89 | require.NoError(t, err) 90 | require.Equal(t, "123", result) 91 | }) 92 | 93 | t.Run("uint64 value", func(t *testing.T) { 94 | result, err := type_cast.ToString(uint64(123)) 95 | require.NoError(t, err) 96 | require.Equal(t, "123", result) 97 | }) 98 | 99 | t.Run("float32 value", func(t *testing.T) { 100 | result, err := type_cast.ToString(float32(123.123)) 101 | require.NoError(t, err) 102 | require.Equal(t, "123.123", result) 103 | }) 104 | 105 | t.Run("float64 value", func(t *testing.T) { 106 | // noinspection ALL 107 | result, err := type_cast.ToString(float64(123.123)) 108 | require.NoError(t, err) 109 | require.Equal(t, "123.123", result) 110 | }) 111 | 112 | t.Run("string value", func(t *testing.T) { 113 | result, err := type_cast.ToString("hello") 114 | require.NoError(t, err) 115 | require.Equal(t, "hello", result) 116 | }) 117 | 118 | t.Run("unsupported value type", func(t *testing.T) { 119 | _, err := type_cast.ToString([]int{1, 2, 3}) 120 | require.Error(t, err) 121 | }) 122 | } 123 | -------------------------------------------------------------------------------- /libs/vendor-proxy/tests/proxy_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "strconv" 21 | "testing" 22 | 23 | "github.com/unpleasantcam/componego/libs/vendor-proxy" 24 | ) 25 | 26 | func TestVendorProxy(t *testing.T) { 27 | VendorProxyTester[*testing.T](t, createInstance) 28 | } 29 | 30 | func BenchmarkVendorProxy(b *testing.B) { 31 | instance := createInstance() 32 | _ = instance.AddFunction("reflectFunc", func(_ int, _ any, _ ...float64) (bool, error) { 33 | return true, nil 34 | }) 35 | _ = instance.AddFunction("contextFunc", func(ctx vendor_proxy.Context, args ...any) (any, error) { 36 | _ = args[0].(int) 37 | _ = args[1] 38 | _ = args[2].([]float64) 39 | ctx.Validated() 40 | return true, nil 41 | }) 42 | arg1 := 1 43 | arg2 := "text" 44 | arg3 := []float64{1.1, 2.2, 3.3, 4.4, 5.5} 45 | b.Run("reflect call", func(b *testing.B) { 46 | b.ReportAllocs() 47 | for n := 0; n < b.N; n++ { 48 | _, _ = instance.CallFunction("reflectFunc", arg1, arg2, arg3) 49 | } 50 | }) 51 | b.Run("context call", func(b *testing.B) { 52 | b.ReportAllocs() 53 | for n := 0; n < b.N; n++ { 54 | _, _ = instance.CallFunction("contextFunc", arg1, arg2, arg3) 55 | } 56 | }) 57 | } 58 | 59 | func createInstance() vendor_proxy.Proxy { 60 | index := 0 61 | for { 62 | name := "temp-vendor-proxy-" + strconv.Itoa(index) 63 | if !vendor_proxy.Has(name) { 64 | return vendor_proxy.Get(name) 65 | } 66 | index++ 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /libs/xerrors/option.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package xerrors 18 | 19 | type Option interface { 20 | Key() string 21 | Value() any 22 | } 23 | 24 | type option struct { 25 | key string 26 | value any 27 | } 28 | 29 | func NewOption(key string, value any) Option { 30 | return &option{ 31 | key: key, 32 | value: value, 33 | } 34 | } 35 | 36 | func (o *option) Key() string { 37 | return o.key 38 | } 39 | 40 | func (o *option) Value() any { 41 | return o.value 42 | } 43 | 44 | type callableOption struct { 45 | key string 46 | getValue func() any 47 | } 48 | 49 | func NewCallableOption(key string, getValue func() any) Option { 50 | return &callableOption{ 51 | key: key, 52 | getValue: getValue, 53 | } 54 | } 55 | 56 | func (c *callableOption) Key() string { 57 | return c.key 58 | } 59 | 60 | func (c *callableOption) Value() any { 61 | return c.getValue() 62 | } 63 | -------------------------------------------------------------------------------- /libs/xerrors/tests/unwrap_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "errors" 21 | "testing" 22 | 23 | "github.com/unpleasantcam/componego/internal/testing/require" 24 | "github.com/unpleasantcam/componego/libs/xerrors" 25 | ) 26 | 27 | func TestUnwrapAll(t *testing.T) { 28 | t.Run("single unwrap error", func(t *testing.T) { 29 | err1 := errors.New("error") 30 | err2 := &singleUnwrapError{message: "wrapped error", inner: err1} 31 | result := xerrors.UnwrapAll(err2) 32 | require.Len(t, result, 2) 33 | require.Same(t, err2, result[0]) 34 | require.Same(t, err1, result[1]) 35 | }) 36 | 37 | t.Run("multiple unwrap error", func(t *testing.T) { 38 | err1 := errors.New("error 1") 39 | err2 := errors.New("error 2") 40 | err3 := &multipleUnwrapError{message: "wrapped error", inners: []error{err1, err2}} 41 | result := xerrors.UnwrapAll(err3) 42 | require.Len(t, result, 3) 43 | require.Same(t, err3, result[0]) 44 | require.Same(t, err2, result[1]) 45 | require.Same(t, err1, result[2]) 46 | }) 47 | 48 | t.Run("mixed unwrap error", func(t *testing.T) { 49 | err1 := errors.New("error") 50 | err2 := &singleUnwrapError{message: "wrapped error 1", inner: err1} 51 | err3 := &multipleUnwrapError{message: "wrapped error 2", inners: []error{err1, err2}} 52 | result := xerrors.UnwrapAll(err3) 53 | require.Len(t, result, 4) 54 | require.Same(t, err3, result[0]) 55 | require.Same(t, err2, result[1]) 56 | require.Same(t, err1, result[2]) 57 | require.Same(t, err1, result[3]) 58 | }) 59 | 60 | t.Run("nil Error", func(t *testing.T) { 61 | var err error 62 | result := xerrors.UnwrapAll(err) 63 | require.Len(t, result, 1) 64 | require.Nil(t, result[0]) 65 | }) 66 | } 67 | 68 | type singleUnwrapError struct { 69 | message string 70 | inner error 71 | } 72 | 73 | func (e *singleUnwrapError) Error() string { 74 | return e.message 75 | } 76 | 77 | func (e *singleUnwrapError) Unwrap() error { 78 | return e.inner 79 | } 80 | 81 | type multipleUnwrapError struct { 82 | message string 83 | inners []error 84 | } 85 | 86 | func (e *multipleUnwrapError) Error() string { 87 | return e.message 88 | } 89 | 90 | func (e *multipleUnwrapError) Unwrap() []error { 91 | return e.inners 92 | } 93 | 94 | var ( 95 | _ error = (*singleUnwrapError)(nil) 96 | _ interface{ Unwrap() error } = (*singleUnwrapError)(nil) 97 | _ error = (*multipleUnwrapError)(nil) 98 | _ interface{ Unwrap() []error } = (*multipleUnwrapError)(nil) 99 | ) 100 | -------------------------------------------------------------------------------- /libs/xerrors/tests/xerrors.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "errors" 21 | "strings" 22 | 23 | "github.com/unpleasantcam/componego/internal/testing" 24 | "github.com/unpleasantcam/componego/libs/xerrors" 25 | ) 26 | 27 | func XErrorsTester[T testing.T]( 28 | t testing.TRun[T], 29 | factory func(message string, code string, options ...xerrors.Option) xerrors.XError, 30 | ) { 31 | err1 := errors.New("error1") 32 | err1x := factory("error1", "E1X") 33 | err2 := errors.New("error2") 34 | err2x := factory("error2", "E2X") 35 | err3 := errors.New("error3") 36 | err3x := factory("error3", "E3X") 37 | err1S3 := errors.Join(err1, err3) 38 | err1xS3x := err1x.WithError(err3x, "E1XS3X") 39 | err1S2 := errors.Join(err1, err2) 40 | err1xS2 := err1x.WithError(err2, "E1XS2") 41 | err1S2S3 := errors.Join(err1S2, err3) 42 | err1xS2S3x := err1xS2.WithError(err3x, "E1XS2S3X") 43 | t.Run("comparison with the original error package", func(t T) { 44 | testCases := [...]struct { 45 | a1 error 46 | b1 error 47 | a2 error 48 | b2 error 49 | }{ 50 | {err1, err1, err1x, err1x}, // 1 51 | {err1, err2, err1x, err2x}, // 2 52 | {err2, err1, err2x, err1x}, // 3 53 | {err1, err3, err1x, err3x}, // 4 54 | {err1, err1S3, err1x, err1xS3x}, // 5 55 | {err1S3, err1, err1xS3x, err1x}, // 6 56 | {err1S3, err2, err1xS3x, err2x}, // 7 57 | {err1S2, err2, err1xS2, err2}, // 8 58 | {err2, err1S2, err2, err1xS2}, // 9 59 | {err1S2S3, err3, err1xS2S3x, err3x}, // 10 60 | {err1S2, err3, err1xS2, err3x}, // 11 61 | } 62 | for i, testCase := range testCases { 63 | if errors.Is(testCase.a1, testCase.b1) != errors.Is(testCase.a2, testCase.b2) { 64 | t.Errorf("#%d failed", i+1) 65 | t.FailNow() 66 | } 67 | } 68 | }) 69 | t.Run("comparison with the expected result", func(t T) { 70 | testCases := [...]struct { 71 | a error 72 | b error 73 | result bool 74 | }{ 75 | {err1x, err1x, true}, // 1 76 | {err1x, err2x, false}, // 2 77 | {err1x, err1, false}, // 3 78 | {err1, err1x, false}, // 4 79 | {err1xS3x, err1x, true}, // 5 80 | {err1xS3x, err3x, true}, // 6 81 | {err1xS3x, err2x, false}, // 7 82 | {err1xS2, err2, true}, // 8 83 | {err2, err1xS2, false}, // 9 84 | {err1xS2, err2, true}, // 10 85 | {err1xS2S3x, err1x, true}, // 11 86 | {err1xS2S3x, err2, true}, // 12 87 | {err1xS2S3x, err3x, true}, // 13 88 | {err1xS2S3x, err3, false}, // 14 89 | {err3x, err1xS2S3x, false}, // 15 90 | } 91 | for i, testCase := range testCases { 92 | if errors.Is(testCase.a, testCase.b) != testCase.result { 93 | t.Errorf("#%d failed", i+1) 94 | t.FailNow() 95 | } 96 | } 97 | }) 98 | t.Run("error text comparison", func(t T) { 99 | testCases2 := [...]struct { 100 | a error 101 | b error 102 | }{ 103 | {err1, err1x}, // 1 104 | {err1S3, err1xS3x}, // 2 105 | {err1S2, err1xS2}, // 3 106 | {err1S2S3, err1xS2S3x}, // 4 107 | } 108 | for i, testCase := range testCases2 { 109 | if testCase.a.Error() != convertErrorMessage(testCase.b.Error()) { 110 | t.Errorf("#%d failed", i+1) 111 | t.FailNow() 112 | } 113 | } 114 | }) 115 | } 116 | 117 | func GetOptionsByKey(t testing.T, err error, key string) any { 118 | for _, err = range xerrors.UnwrapAll(err) { 119 | // noinspection ALL 120 | xError, ok := err.(xerrors.XError) //nolint:errorlint 121 | if !ok { 122 | continue 123 | } 124 | for _, option := range xError.ErrorOptions() { 125 | if option.Key() == key { 126 | return option.Value() 127 | } 128 | } 129 | } 130 | t.Errorf("failed to get error option by key '%s'", key) 131 | t.FailNow() 132 | return nil 133 | } 134 | 135 | func convertErrorMessage(errMessage string) string { 136 | lines := strings.Split(errMessage, "\n") 137 | for i, line := range lines { 138 | index := strings.Index(line, " (") 139 | if index != -1 { 140 | lines[i] = line[:index] 141 | } 142 | } 143 | return strings.Join(lines, "\n") 144 | } 145 | -------------------------------------------------------------------------------- /libs/xerrors/tests/xerrors_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tests 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/unpleasantcam/componego/libs/xerrors" 23 | ) 24 | 25 | func TestXErrors(t *testing.T) { 26 | XErrorsTester[*testing.T](t, xerrors.New) 27 | } 28 | -------------------------------------------------------------------------------- /libs/xerrors/unwrap.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package xerrors 18 | 19 | func UnwrapAll(err error) []error { 20 | result := make([]error, 0, 5) 21 | errorStack := make([]error, 0, 5) 22 | errorStack = append(errorStack, err) 23 | for len(errorStack) > 0 { 24 | err = errorStack[len(errorStack)-1] 25 | result = append(result, err) 26 | errorStack = errorStack[:len(errorStack)-1] 27 | switch castedErr := err.(type) { //nolint:errorlint 28 | case interface{ Unwrap() error }: 29 | errorStack = append(errorStack, castedErr.Unwrap()) 30 | case interface{ Unwrap() []error }: 31 | errorStack = append(errorStack, castedErr.Unwrap()...) 32 | } 33 | } 34 | return result 35 | } 36 | -------------------------------------------------------------------------------- /libs/xerrors/xerrors.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package xerrors 18 | 19 | import ( 20 | "errors" 21 | ) 22 | 23 | type XError interface { 24 | error 25 | ErrorCode() string 26 | ErrorOptions() []Option 27 | WithMessage(message string, code string, options ...Option) XError 28 | WithError(err error, code string, options ...Option) XError 29 | WithOptions(code string, options ...Option) XError 30 | } 31 | 32 | type xError struct { 33 | parent XError 34 | current error 35 | asMessage bool 36 | code string 37 | options []Option 38 | } 39 | 40 | func New(message string, code string, options ...Option) XError { 41 | return &xError{ 42 | parent: nil, 43 | current: errors.New(message), 44 | asMessage: true, 45 | code: code, 46 | options: options, 47 | } 48 | } 49 | 50 | func ToXError(err error, code string, options ...Option) XError { 51 | return &xError{ 52 | parent: nil, 53 | current: err, 54 | asMessage: false, 55 | code: code, 56 | options: options, 57 | } 58 | } 59 | 60 | func (x *xError) Error() string { 61 | if x.parent == nil { 62 | if x.code == "" { 63 | return x.current.Error() 64 | } 65 | return x.current.Error() + " (" + x.code + ")" 66 | } 67 | if x.code == "" { 68 | return x.parent.Error() + "\n" + x.current.Error() 69 | } 70 | return x.parent.Error() + "\n" + x.current.Error() + " (" + x.code + ")" 71 | } 72 | 73 | func (x *xError) ErrorCode() string { 74 | return x.code 75 | } 76 | 77 | func (x *xError) ErrorOptions() []Option { 78 | return x.options 79 | } 80 | 81 | func (x *xError) WithMessage(message string, code string, options ...Option) XError { 82 | return &xError{ 83 | parent: x, 84 | current: errors.New(message), 85 | asMessage: true, 86 | code: code, 87 | options: options, 88 | } 89 | } 90 | 91 | func (x *xError) WithError(err error, code string, options ...Option) XError { 92 | return &xError{ 93 | parent: x, 94 | current: err, 95 | asMessage: false, 96 | code: code, 97 | options: options, 98 | } 99 | } 100 | 101 | func (x *xError) WithOptions(code string, options ...Option) XError { 102 | return &xError{ 103 | parent: nil, 104 | current: x, 105 | asMessage: false, 106 | code: code, 107 | options: options, 108 | } 109 | } 110 | 111 | func (x *xError) Unwrap() []error { 112 | if x.parent == nil { 113 | if x.asMessage { 114 | return nil 115 | } 116 | return []error{x.current} 117 | } 118 | if x.asMessage { 119 | return []error{x.parent} 120 | } 121 | return []error{x.parent, x.current} 122 | } 123 | 124 | var ( 125 | _ error = (*xError)(nil) 126 | _ XError = (*xError)(nil) 127 | _ interface{ Unwrap() []error } = (*xError)(nil) 128 | ) 129 | -------------------------------------------------------------------------------- /processor.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-present Volodymyr Konstanchuk and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package componego 18 | 19 | // Processor is an interface that describes the validation or conversion of data to another format. 20 | type Processor interface { 21 | // ProcessData validates and converts data. 22 | // The method returns a new value or an error. 23 | ProcessData(value any) (any, error) 24 | } 25 | -------------------------------------------------------------------------------- /scripts/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22-alpine 2 | 3 | ENV PIP_ROOT_USER_ACTION=ignore 4 | 5 | RUN apk add --update bash git make gcc libc-dev binutils-gold && \ 6 | apk add --update --no-cache python3-dev~3.10 --repository=https://dl-cdn.alpinelinux.org/alpine/v3.17/main && \ 7 | ln -sf python3 /usr/bin/python && \ 8 | python3 -m ensurepip && \ 9 | pip3 install --no-cache --upgrade pip setuptools pre-commit 10 | 11 | CMD ["sleep", "infinity"] 12 | -------------------------------------------------------------------------------- /tools/create-contributor-env.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Copyright 2024-present Volodymyr Konstanchuk and contributors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | 20 | check_directory() { 21 | if [ -e "$1" ]; then 22 | echo "ERROR: could not create a project in the current directory because we are inside another project: $1" 23 | exit 1 24 | fi 25 | } 26 | 27 | directory=$(pwd) 28 | while [ "$directory" != "/" ]; do 29 | check_directory "$directory/go.mod" 30 | check_directory "$directory/.git" 31 | directory=$(dirname "$directory") 32 | done 33 | 34 | mkdir -p "componego-contributor-env" 35 | cd "componego-contributor-env" 36 | 37 | cat >docker-compose.yml <.gitattributes <.gitignore <.editorconfig <.devcontainer/devcontainer.json <src/README.md < $(pwd)" 140 | echo "In the next step, run the following commands manually:" 141 | echo ">$" 142 | echo ">$ ${COMMAND_COLOR}cd $(pwd)${RESET_COLOR}" 143 | echo ">$ ${COMMAND_COLOR}git clone https://github.com/unpleasantcam/componego.git src/componego${RESET_COLOR} # replace repo with your fork if your have one" 144 | echo ">$ ${COMMAND_COLOR}docker-compose up componego-framework -d${RESET_COLOR} # start docker container in background" 145 | echo ">$ ${COMMAND_COLOR}docker inspect --format '{{json .State.Running}}' componego-framework${RESET_COLOR} # check if docker container is running" 146 | echo ">$ ${COMMAND_COLOR}docker exec -ti componego-framework /bin/bash${RESET_COLOR} # open terminal inside docker container" 147 | echo ">$ ${COMMAND_COLOR}cd componego${RESET_COLOR} # change folder inside docker container" 148 | echo ">$ ${COMMAND_COLOR}make tests${RESET_COLOR} # run tests inside docker container" 149 | echo ">$" 150 | --------------------------------------------------------------------------------