├── .cargo └── config.toml ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ └── CICD.yml ├── .gitignore ├── .pep8 ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── README.zh-cn.md ├── assets ├── aifadian.jpg ├── banner.jpg ├── logo.png ├── memory.png └── paypal_button.svg ├── build.rs ├── crates ├── core │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── lib.rs │ │ ├── runtime │ │ ├── context │ │ │ ├── localfs.rs │ │ │ ├── memory.rs │ │ │ └── mod.rs │ │ ├── engine.rs │ │ ├── env.rs │ │ ├── eval.rs │ │ ├── flow.rs │ │ ├── group.rs │ │ ├── js │ │ │ ├── mod.rs │ │ │ └── util.rs │ │ ├── mod.rs │ │ ├── model │ │ │ ├── eid.rs │ │ │ ├── error.rs │ │ │ ├── json │ │ │ │ ├── deser.rs │ │ │ │ ├── helpers.rs │ │ │ │ ├── mod.rs │ │ │ │ └── npdeser.rs │ │ │ ├── mod.rs │ │ │ ├── msg.rs │ │ │ ├── propex.rs │ │ │ ├── red_types.rs │ │ │ ├── settings.rs │ │ │ └── variant │ │ │ │ ├── array.rs │ │ │ │ ├── converts.rs │ │ │ │ ├── js_support.rs │ │ │ │ ├── map.rs │ │ │ │ ├── mod.rs │ │ │ │ └── ser.rs │ │ ├── nodes │ │ │ ├── common_nodes │ │ │ │ ├── catch.rs │ │ │ │ ├── complete.rs │ │ │ │ ├── console_json.rs │ │ │ │ ├── debug.rs │ │ │ │ ├── inject.rs │ │ │ │ ├── junction.rs │ │ │ │ ├── link_call.rs │ │ │ │ ├── link_in.rs │ │ │ │ ├── link_out.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── status.rs │ │ │ │ ├── subflow.rs │ │ │ │ ├── test_once.rs │ │ │ │ └── unknown.rs │ │ │ ├── function_nodes │ │ │ │ ├── change.rs │ │ │ │ ├── function │ │ │ │ │ ├── context_class.rs │ │ │ │ │ ├── edgelink_class.rs │ │ │ │ │ ├── env_class.rs │ │ │ │ │ ├── function.prelude.js │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── node_class.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── range.rs │ │ │ │ └── rbe.rs │ │ │ ├── mod.rs │ │ │ └── network_nodes │ │ │ │ ├── mod.rs │ │ │ │ └── udp_out.rs │ │ ├── registry.rs │ │ └── subflow.rs │ │ ├── text │ │ ├── json.rs │ │ ├── json_seq.rs │ │ ├── mod.rs │ │ ├── nom_parsers.rs │ │ ├── parsing.rs │ │ └── regex.rs │ │ └── utils │ │ ├── async_util.rs │ │ ├── graph.rs │ │ ├── mod.rs │ │ ├── time.rs │ │ └── topo.rs ├── macro │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── pymod │ ├── Cargo.toml │ └── src │ ├── json.rs │ └── lib.rs ├── edgelinkd.dev.toml ├── edgelinkd.prod.toml ├── edgelinkd.toml ├── log.toml ├── node-plugins └── edgelink-nodes-dummy │ ├── Cargo.toml │ └── src │ └── lib.rs ├── pytest.ini ├── rustfmt.toml ├── scripts ├── cross-unittest.py ├── gen-red-node-specs.sh ├── load-pymod-test.py ├── specs_diff.json ├── specs_diff.py └── udp_server.py ├── src ├── cliargs.rs ├── consts.rs ├── logging.rs └── main.rs └── tests ├── REDNODES-SPECS-DIFF.md ├── __init__.py ├── conftest.py ├── data └── flows.json ├── home └── edgelinkd.toml ├── nodes ├── __init__.py ├── common │ ├── __init__.py │ ├── test_catch_node.py │ ├── test_inject_node.py │ ├── test_junction_node.py │ └── test_link_nodes.py ├── function │ ├── test_change_node.py │ ├── test_delay_node.py │ ├── test_function_node.py │ ├── test_range_node.py │ ├── test_rbe_node.py │ ├── test_switch_node.py │ ├── test_template_node.py │ └── test_trigger_node.py └── test_subflow.py └── requirements.txt /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | EDGELINK_HOME = "./" 3 | EDGELINK_RUN_ENV = "dev" 4 | CLICOLOR_FORCE="1" 5 | 6 | [build] 7 | #rustflags='-C prefer-dynamic' 8 | 9 | [target.x86_64-pc-windows-msvc] 10 | linker = "rust-lld.exe" 11 | rustdocflags = ["-Clinker=rust-lld.exe"] 12 | 13 | # This cfg will reduce the size of `windows::core::Error` from 16 bytes to 4 bytes 14 | [target.'cfg(target_os = "windows")'] 15 | rustflags = ["--cfg", "windows_slim_errors"] 16 | 17 | [target.armv7-unknown-linux-gnueabihf] 18 | linker = "arm-linux-gnueabihf-gcc" 19 | 20 | [target.aarch64-unknown-linux-gnu] 21 | linker = "aarch64-linux-gnu-gcc" 22 | 23 | [target.armv7-unknown-linux-gnueabi] 24 | linker = "arm-linux-gnueabi-gcc" -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at oldrev@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at http://contributor-covenant.org/version/1/4 44 | 45 | ## Contact 46 | 47 | For any questions or concerns regarding the Code of Conduct, please contact us at oldrev@gmail.com. -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to EdgeLink 2 | 3 | Thank you for considering contributing to the EdgeLink! Contributions are essential to making open source projects successful. 4 | 5 | ## How to Contribute 6 | 7 | 1. Fork the repository. 8 | 9 | 2. Clone the forked repository to your local machine: 10 | 11 | ```bash 12 | git clone https://github.com/edge-link/edgelink.rs.git 13 | cd edgelink.rs 14 | ``` 15 | 16 | 3. Create a new branch for your feature or bug fix: 17 | 18 | ```bash 19 | git checkout -b feature-name 20 | ``` 21 | 22 | 4. Make your changes, and ensure they follow the coding style and guidelines. 23 | 24 | 5. Test your changes thoroughly. 25 | 26 | 6. Commit your changes: 27 | 28 | ```bash 29 | git commit -m "Your descriptive commit message" 30 | ``` 31 | 32 | 7. Push your changes to your forked repository: 33 | 34 | ```bash 35 | git push origin feature-name 36 | ``` 37 | 38 | 8. Create a pull request (PR) on the main repository. 39 | 40 | ## Code Style and Guidelines 41 | 42 | - Follow the existing code style used in the project. 43 | 44 | - Keep code changes focused and small for easier review. 45 | 46 | - Provide clear and detailed commit messages. 47 | 48 | ## Reporting Issues 49 | 50 | If you find a bug or have a feature request, please open an issue on the [issue tracker](https://github.com/edgelink/edgelink.rs/issues). Ensure that you provide as much detail as possible, including steps to reproduce the issue. 51 | 52 | ## Code of Conduct 53 | 54 | Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project, you agree to abide by its terms. 55 | 56 | ## Contact 57 | 58 | If you have any questions or need clarification, feel free to reach out to the project maintainers through [email@example.com](mailto:oldrev@gmail.com) or open a discussion on the [GitHub Discussions](https://github.com/edgelink/edgelink.rs/discussions) page. 59 | 60 | ## Thank You! 61 | 62 | Thank you for contributing to the Node-RED Rust Backend! Your time and effort are greatly appreciated. 63 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: oldrev 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['paypal.me/oldrev'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /vendor 3 | /log 4 | __pycache__ 5 | /.pytest_cache -------------------------------------------------------------------------------- /.pep8: -------------------------------------------------------------------------------- 1 | [pep8] 2 | max-line-length = 120 3 | ignore = E226,E302,E41 -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug executable 'edgelink-app'", 11 | "cargo": { 12 | "args": [ 13 | "build", 14 | "--bin=edgelinkd", 15 | "--package=edgelink-app" 16 | ], 17 | "filter": { 18 | "name": "edgelinkd", 19 | "kind": "bin" 20 | } 21 | }, 22 | "args": [ 23 | "-v", 24 | "4" 25 | ], 26 | "env": { 27 | "EDGELINK_HOME": "${workspaceFolder}" 28 | }, 29 | "cwd": "${workspaceFolder}", 30 | "console": "integratedTerminal", 31 | "internalConsoleOptions": "neverOpen", 32 | }, 33 | { 34 | "type": "lldb", 35 | "request": "launch", 36 | "name": "Debug unit tests in executable 'edgelink-app'", 37 | "cargo": { 38 | "args": [ 39 | "test", 40 | "--no-run", 41 | "--bin=edgelinkd", 42 | "--package=edgelink-app" 43 | ], 44 | "filter": { 45 | "name": "edgelinkd", 46 | "kind": "bin" 47 | } 48 | }, 49 | "args": [], 50 | "cwd": "${workspaceFolder}", 51 | "console": "integratedTerminal" 52 | }, 53 | { 54 | "type": "lldb", 55 | "request": "launch", 56 | "name": "Debug unit tests in library 'edgelink'", 57 | "cargo": { 58 | "args": [ 59 | "test", 60 | "--no-run", 61 | "--lib", 62 | "--color always", 63 | "--package=edgelink" 64 | ], 65 | "filter": { 66 | "name": "edgelink", 67 | "kind": "lib" 68 | } 69 | }, 70 | "args": [], 71 | "cwd": "${workspaceFolder}", 72 | "console": "integratedTerminal" 73 | }, 74 | { 75 | "type": "lldb", 76 | "request": "launch", 77 | "name": "Debug integration test 'all'", 78 | "cargo": { 79 | "args": [ 80 | "test", 81 | "--all", 82 | ] 83 | }, 84 | "args": [], 85 | "cwd": "${workspaceFolder}", 86 | "console": "integratedTerminal" 87 | } 88 | ] 89 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.checkOnSave": false, 3 | "rust-analyzer.typing.continueCommentsOnNewline": false 4 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "cargo build", 6 | "type": "shell", 7 | "command": "cargo build", 8 | "args": [ 9 | "--color", 10 | "always" 11 | ], 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | }, 17 | { 18 | "label": "cargo clippy", 19 | "type": "shell", 20 | "command": "cargo", 21 | "args": [ 22 | "clippy", 23 | "--all", 24 | "--color", 25 | "always" 26 | ], 27 | "group": { 28 | "kind": "build", 29 | "isDefault": true 30 | } 31 | }, 32 | { 33 | "label": "cargo nextest", 34 | "type": "shell", 35 | "command": "cargo", 36 | "args": [ 37 | "nextest", 38 | "run", 39 | "--all" 40 | ], 41 | "group": { 42 | "kind": "build", 43 | "isDefault": true 44 | } 45 | }, 46 | { 47 | "label": "cargo run", 48 | "type": "shell", 49 | "command": "cargo", 50 | "args": [ 51 | "run", 52 | "--color", 53 | "always" 54 | // "--release", 55 | // "--", 56 | // "arg1" 57 | ], 58 | "group": { 59 | "kind": "build", 60 | "isDefault": true 61 | } 62 | }, 63 | { 64 | "type": "shell", 65 | "label": "cargo test build", 66 | "command": "cargo", 67 | "args": [ 68 | "test", 69 | "--no-run" 70 | ], 71 | "problemMatcher": [ 72 | "$rustc" 73 | ] 74 | } 75 | ] 76 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to EdgeLink 2 | 3 | Thank you for considering contributing to EdgeLink! We appreciate your interest and effort in helping to improve this project. This document outlines the guidelines and steps to follow when contributing to the project. 4 | 5 | ## Table of Contents 6 | 7 | 1. [Code of Conduct](#code-of-conduct) 8 | 2. [How Can I Contribute?](#how-can-i-contribute) 9 | - [Reporting Bugs](#reporting-bugs) 10 | - [Suggesting Enhancements](#suggesting-enhancements) 11 | - [Pull Requests](#pull-requests) 12 | 3. [Development Setup](#development-setup) 13 | 4. [Coding Guidelines](#coding-guidelines) 14 | 5. [Commit Guidelines](#commit-guidelines) 15 | 6. [Issue and Pull Request Labels](#issue-and-pull-request-labels) 16 | 7. [Community](#community) 17 | 18 | ## Code of Conduct 19 | 20 | By participating in this project, you are expected to uphold our [Code of Conduct](CODE_OF_CONDUCT.md). Please report any unacceptable behavior to me: oldrev@gmail.com. 21 | 22 | ## How Can I Contribute? 23 | 24 | ### Reporting Bugs 25 | 26 | Before submitting a bug report, please check the [existing issues](https://github.com/oldrev/edgelink/issues) to see if the problem has already been reported. If it hasn't, you can create a new issue. 27 | 28 | #### How Do I Submit a Good Bug Report? 29 | 30 | - **Use a clear and descriptive title** for the issue to identify the problem. 31 | - **Describe the exact steps** which reproduce the problem in as many details as possible. 32 | - **Provide specific examples** to demonstrate the steps. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. 33 | - **Describe the behavior** you observed after following the steps and point out what exactly is the problem with that behavior. 34 | - **Explain which behavior you expected** to see instead and why. 35 | - **Include screenshots or videos** which show you following the described steps and clearly demonstrate the problem. 36 | 37 | ### Suggesting Enhancements 38 | 39 | If you have an idea for a new feature or an improvement to an existing one, we'd love to hear about it! Here's how you can suggest enhancements: 40 | 41 | #### How Do I Submit a Good Enhancement Suggestion? 42 | 43 | - **Use a clear and descriptive title** for the issue to identify the suggestion. 44 | - **Provide a step-by-step description** of the suggested enhancement in as many details as possible. 45 | - **Describe the current behavior** and explain which behavior you expected to see instead and why. 46 | - **Include screenshots and animated GIFs** which help you demonstrate the steps or point out the part of the project the suggestion is related to. 47 | - **Explain why this enhancement would be useful** to most users. 48 | 49 | ### Pull Requests 50 | 51 | 1. **Fork the repository** and create your branch from `master`. 52 | 2. **Make your changes** in a new git branch: 53 | ``` 54 | git checkout -b feature/your-feature-name 55 | ``` 56 | 3. **Follow our coding guidelines** (see [Coding Guidelines](#coding-guidelines)). 57 | 4. **Ensure the test suite passes**. 58 | 5. **Make sure your code pass static checks, including `cargo fmt --all --check` and `cargo clippy --all`**. 59 | 6. **Commit your changes** using a descriptive commit message (see [Commit Guidelines](#commit-guidelines)). 60 | 7. **Push your branch** to GitHub: 61 | ``` 62 | git push origin feature/your-feature-name 63 | ``` 64 | 8. **Submit a pull request** to the `main` branch. 65 | 66 | ## Development Setup 67 | 68 | To set up your development environment, follow these steps: 69 | 70 | 1. **Clone the repository**: 71 | ``` 72 | git clone https://github.com/oldrev/edgelink.git 73 | ``` 74 | 2. **Install dependencies for Python tests (Optional)**: 75 | ``` 76 | cd edgelink 77 | pip -U -r ./tests/requirements.txt 78 | ``` 79 | 3. **Run the CLI program**: 80 | ``` 81 | cargo run 82 | ``` 83 | 84 | ## Coding Guidelines 85 | 86 | - **Follow the existing code style**. Consistency is key. 87 | - **Write clear and concise code**. Comment your code where necessary. 88 | - **Write tests** for your code and ensure all tests pass before submitting a pull request. 89 | 90 | ## Commit Guidelines 91 | 92 | - Use the present tense ("Add feature" not "Added feature"). 93 | - Use the imperative mood ("Move cursor to..." not "Moves cursor to..."). 94 | - Limit the first line to 72 characters or less. 95 | - Reference issues and pull requests liberally after the first line. 96 | 97 | ## Issue and Pull Request Labels 98 | 99 | We use labels to categorize issues and pull requests. Here are some of the labels you might see: 100 | 101 | - **bug**: Something isn't working 102 | - **enhancement**: New feature or request 103 | - **documentation**: Improvements or additions to documentation 104 | - **good first issue**: Good for newcomers 105 | - **help wanted**: Extra attention is needed 106 | 107 | ## Community 108 | 109 | Feel free to join our community discussions on [Discord](https://discord.gg/TODO). We're always happy to help and discuss new ideas! 110 | 111 | --- 112 | 113 | Thank you for contributing to EdgeLink! Your efforts help make this project better for everyone. -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "edgelink-app" 3 | version = "0.1.0" 4 | edition = "2021" 5 | rust-version = "1.77.2" 6 | readme = "README.md" 7 | homepage = "https://github.com/oldrev/edgelink.rs" 8 | description = "EdgeLink is a Node-RED compatible run-time engine in Rust." 9 | build = "build.rs" 10 | license = "Apache 2.0" 11 | authors = ["Li Wei "] 12 | 13 | 14 | [[bin]] 15 | name = "edgelinkd" 16 | path = "src/main.rs" 17 | 18 | 19 | # Enable a small amount of optimization in debug mode 20 | [profile.dev] 21 | opt-level = 0 22 | 23 | [profile.ci] 24 | inherits = "release" 25 | debug = false 26 | incremental = false 27 | 28 | [profile.release] 29 | opt-level = "z" # Optimize for size. 30 | lto = true # Enable Link Time Optimization 31 | codegen-units = 1 # Reduce number of codegen units to increase optimizations. 32 | strip = true # Automatically strip symbols from the binary. 33 | 34 | [profile.test] 35 | opt-level = 1 # Enables thin local LTO and some optimizations. 36 | 37 | [workspace.dependencies] 38 | bincode = "1" 39 | async-trait = "0.1" 40 | anyhow = { version = "1", features = ["backtrace"] } 41 | log = "0.4" 42 | tokio = "1" 43 | tokio-util = "0.7" 44 | semver = "1" 45 | config = { version = "0.14", default-features = false, features = [ 46 | "convert-case", 47 | "toml", 48 | ] } 49 | serde = { version = "1" } 50 | serde_json = "1" 51 | dashmap = { version = "6", features = ["serde"] } 52 | rand = "0.8" 53 | base64 = "0.22" 54 | bytes = { version = "1", features = ["std", "serde"] } 55 | chrono = "0.4" 56 | regex = "1" 57 | thiserror = "1" 58 | nom = "7" 59 | tokio-cron-scheduler = "0.11" 60 | bumpalo = "3" 61 | dirs-next = "2" 62 | clap = { version = "4", features = ["derive"] } 63 | itertools = "0.13" 64 | arrayvec = "0.7" 65 | smallvec = "1" 66 | smallstr = { version = "0.3", features = ["serde", "std", "union"] } 67 | inventory = "0.3" 68 | rquickjs = { version = "0.6", features = [ 69 | "chrono", 70 | "loader", 71 | "allocator", 72 | "either", 73 | "classes", 74 | "properties", 75 | "array-buffer", 76 | "macro", 77 | "futures", 78 | "parallel", 79 | ] } 80 | #llrt_modules = { git = "https://github.com/awslabs/llrt.git", default-features = false, package = "llrt_modules", features = ["buffer", "timers"]} 81 | rquickjs-extra = { git = "https://github.com/rquickjs/rquickjs-extra.git", rev = "c838e60", default-features = false, features = [ 82 | "timers", 83 | "console", 84 | ] } 85 | log4rs = { version = "1", features = [ 86 | "console_appender", 87 | "file_appender", 88 | "rolling_file_appender", 89 | "compound_policy", 90 | "delete_roller", 91 | "fixed_window_roller", 92 | "size_trigger", 93 | "time_trigger", 94 | # "json_encoder", 95 | "pattern_encoder", 96 | "threshold_filter", 97 | "config_parsing", 98 | "toml_format", 99 | ], default-features = false } 100 | ctor = "0.2.8" 101 | 102 | [dependencies] 103 | clap.workspace = true 104 | dirs-next.workspace = true 105 | anyhow.workspace = true 106 | log.workspace = true 107 | tokio = { workspace = true, features = ["signal"] } 108 | tokio-util.workspace = true 109 | config.workspace = true 110 | semver.workspace = true 111 | serde_json.workspace = true 112 | serde = { workspace = true, features = ["derive"] } 113 | log4rs.workspace = true 114 | 115 | edgelink-core = { path = "crates/core", default-features = false } 116 | 117 | # Node plug-ins: 118 | edgelink-nodes-dummy = { path = "node-plugins/edgelink-nodes-dummy" } 119 | 120 | 121 | [dev-dependencies] 122 | 123 | [workspace] 124 | members = ["crates/*", "node-plugins/*"] 125 | 126 | [package.metadata.bundle] 127 | identifier = "com.github.oldrev.edgelink" 128 | 129 | [features] 130 | full = ["default", "rqjs_bindgen"] 131 | default = ["core", "js"] 132 | core = ["edgelink-core/core"] 133 | js = ["edgelink-core/js"] 134 | rqjs_bindgen = ["js", "edgelink-core/rqjs_bindgen"] 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EdgeLink: A Node-RED Compatible Run-time Engine in Rust 2 | [![Build Status]][actions] 3 | [![Releases](https://img.shields.io/github/release/oldrev/edgelink.svg)](https://github.com/oldrev/edgelink/releases) 4 | 5 | [Build Status]: https://img.shields.io/github/actions/workflow/status/oldrev/edgelink/CICD.yml?branch=master 6 | [actions]: https://github.com/oldrev/edgelink/actions?query=branch%3Amaster 7 | 8 | ![Node-RED Rust Backend](assets/banner.jpg) 9 | 10 | English | [简体中文](README.zh-cn.md) 11 | 12 | ## Overview 13 | 14 | EdgeLink is a [Node-RED](https://nodered.org/) compatible run-time engine implemented in Rust. 15 | 16 | This program is designed to execute `flows.json` file that have been designed and exported/deployed using Node-RED, without any editor or other HTML/Web-related functionalities. The purpose of its development is to deploy tested Node-RED flows to devices with limited memory for execution. 17 | 18 | Only the "function" node will use the lightweight QuickJS JS interpreter to run their code; all other functionalities are implemented in native Rust code. 19 | 20 | ## Features 21 | 22 | ![Memory Usage](assets/memory.png) 23 | 24 | - **High Performance**: Leverage the advantages of the Rust language for excellent performance. 25 | - **Low Memory Footprint**: Reduce memory usage compared to the NodeJS backend. Tests indicate that, for running a same simple workflow, the physical memory usage of EdgeLink is only 10% of that of Node-RED. 26 | - **Scalability**: Retain the extensibility of Node-RED, supporting custom nodes. 27 | - **Easy Migration**: Easily replace the existing Node-RED backend with minimal modifications. 28 | 29 | ## Quick Start 30 | 31 | ### 0. Install Node-RED 32 | 33 | For the purpose of testing this project, we first need to install Node-RED as our flow designer and generate the `flows.json` file. Please refer to the Node-RED documentation for its installation and usage. 34 | 35 | After completing the flow design in Node-RED, please ensure that you click the big red "Deploy" button to generate the `flows.json` file. By default, this file is located in `~/.node-red/flows.json`. Be mindful not to use Node-RED features that are not yet implemented in this project. 36 | 37 | ### 1. Build 38 | 39 | Using Rust 1.80 or later, run: 40 | 41 | ```bash 42 | cargo build -r 43 | ``` 44 | 45 | > [!IMPORTANT] 46 | > **Note for Windows Users:** 47 | > Windows users should ensure that the `patch.exe` program is available in the `%PATH%` environment variable to successfully compile the project using `rquickjs`. This utility is required to apply patches to the QuickJS library for Windows compatibility. If Git is already installed, it will include `patch.exe`. 48 | > 49 | > To compile `rquickjs`, which is required by the project, you will need to install Microsoft Visual C++ (MSVC) and the corresponding Windows Software Development Kit (SDK). 50 | 51 | The toolchains tested are as follows(see GitHub Actions for details): 52 | 53 | * `x86_64-pc-windows-msvc` 54 | * `x86_64-pc-windows-gnu` 55 | * `x86_64-unknown-linux-gnu` 56 | * `aarch64-unknown-linux-gnu` 57 | * `armv7-unknown-linux-gnueabihf` 58 | * `armv7-unknown-linux-gnueabi` 59 | 60 | ### 2. Run 61 | 62 | ```bash 63 | cargo run -r 64 | ``` 65 | 66 | Or: 67 | 68 | ```bash 69 | ./target/release/edgelinkd 70 | ``` 71 | 72 | By default, EdgeLink will read `~/.node-red/flows.json` and execute it. 73 | 74 | You can use the `--help` command-line argument to view all the supported options for this program: 75 | 76 | ```bash 77 | ./target/release/edgelinkd --help 78 | ``` 79 | 80 | #### Run Unit Tests 81 | 82 | ```bash 83 | cargo test --all 84 | ``` 85 | 86 | #### Run Integration Tests 87 | 88 | Running integration tests requires first installing Python 3.9+ and the corresponding Pytest dependencies: 89 | 90 | ```bash 91 | pip install -r ./tests/requirements.txt 92 | ``` 93 | 94 | Then execute the following command: 95 | 96 | ```bash 97 | set PYO3_PYTHON=YOUR_PYTHON_EXECUTABLE_PATH # Windows only 98 | cargo build --all 99 | py.test 100 | ``` 101 | 102 | ## Configuration 103 | 104 | Adjust various settings and configuration, please execute `edgelinkd` with flags. 105 | The flags available can be found when executing `edgelinkd --help`. 106 | 107 | ## Project Status 108 | 109 | **Pre-Alpha Stage**: The project is currently in the *pre-alpha* stage and cannot guarantee stable operation. 110 | 111 | The heavy check mark ( :heavy_check_mark: ) below indicates that this feature has passed the integration test ported from Node-RED. 112 | 113 | ### Node-RED Features Roadmap: 114 | 115 | - [x] :heavy_check_mark: Flow 116 | - [x] :heavy_check_mark: Sub-flow 117 | - [x] Group 118 | - [x] :heavy_check_mark: Environment Variables 119 | - [ ] Context 120 | - [x] Memory storage 121 | - [ ] Local file-system storage 122 | - [ ] RED.util (WIP) 123 | - [x] `RED.util.cloneMessage()` 124 | - [x] `RED.util.generateId()` 125 | - [x] Plug-in subsystem[^1] 126 | - [ ] JSONata 127 | 128 | [^1]: Rust's Tokio async functions cannot call into dynamic libraries, so currently, we can only use statically linked plugins. I will evaluate the possibility of adding plugins based on WebAssembly (WASM) or JavaScript (JS) in the future. 129 | 130 | ### The Current Status of Nodes: 131 | 132 | Refer [REDNODES-SPECS-DIFF.md](tests/REDNODES-SPECS-DIFF.md) to view the details of the currently implemented nodes that comply with the Node-RED specification tests. 133 | 134 | - Core nodes: 135 | - Common nodes: 136 | - [x] :heavy_check_mark: Console-JSON (For integration tests) 137 | - [x] :heavy_check_mark: Inject 138 | - [x] Debug (WIP) 139 | - [x] :heavy_check_mark: Complete 140 | - [x] Catch 141 | - [x] Status 142 | - [x] :heavy_check_mark: Link In 143 | - [x] :heavy_check_mark: Link Call 144 | - [x] :heavy_check_mark: Link Out 145 | - [x] :heavy_check_mark: Comment (Ignored automatically) 146 | - [x] GlobalConfig (WIP) 147 | - [x] :heavy_check_mark: Unknown 148 | - [x] :heavy_check_mark: Junction 149 | - Function nodes: 150 | - [x] Function (WIP) 151 | - [x] Basic functions 152 | - [x] `node` object (WIP) 153 | - [x] `context` object 154 | - [x] `flow` object 155 | - [x] `global` object 156 | - [x] `RED.util` object 157 | - [x] `env` object 158 | - [ ] Switch 159 | - [x] :heavy_check_mark: Change 160 | - [x] :heavy_check_mark: Range 161 | - [ ] Template 162 | - [ ] Delay 163 | - [ ] Trigger 164 | - [ ] Exec 165 | - [x] :heavy_check_mark: Filter (RBE) 166 | - Network nodes: 167 | - [ ] MQTT In 168 | - [ ] MQTT Out 169 | - [ ] HTTP In 170 | - [ ] HTTP Response 171 | - [ ] HTTP Request 172 | - [ ] WebSocket In 173 | - [ ] WebSocket Out 174 | - [ ] TCP In 175 | - [ ] TCP Out 176 | - [ ] TCP Request 177 | - [ ] UDP In 178 | - [x] UDP Out 179 | - [x] Unicast 180 | - [ ] Multicast (WIP) 181 | - [ ] TLS 182 | - [ ] HTTP Proxy 183 | - Sqeuence nodes: 184 | - [ ] Split 185 | - [ ] Join 186 | - [ ] Sort 187 | - [ ] Batch 188 | - Parse nodes: 189 | - [ ] CSV 190 | - [ ] HTML 191 | - [ ] JSON 192 | - [ ] XML 193 | - [ ] YAML 194 | - Storage 195 | - [ ] Write File 196 | - [ ] Read File 197 | - [ ] Watch 198 | 199 | ## Roadmap 200 | 201 | Check out our [milestones](https://github.com/oldrev/edgelink/milestones) to get a glimpse of the upcoming features and milestones. 202 | 203 | ## Contribution 204 | 205 | ![Alt](https://repobeats.axiom.co/api/embed/cd18a784e88be20d79778703bda8858523c4257e.svg "Repobeats analytics image") 206 | 207 | Contributions are always welcome! Please read [CONTRIBUTING.md](.github/CONTRIBUTING.md) for more details. 208 | 209 | If you want to support the development of this project, you could consider buying me a beer. 210 | 211 | Buy Me a Coffee at ko-fi.com 212 | 213 | [![Support via PayPal.me](assets/paypal_button.svg)](https://www.paypal.me/oldrev) 214 | 215 | ## Issues, Feedback and Support 216 | 217 | We welcome your feedback! If you encounter any issues or have suggestions, please open an [issue](https://github.com/edge-link/edgelink/issues). 218 | 219 | E-mail: oldrev(at)gmail.com 220 | 221 | ## License 222 | 223 | This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for more details. 224 | 225 | Copyright © Li Wei and other contributors. All rights reserved. 226 | -------------------------------------------------------------------------------- /README.zh-cn.md: -------------------------------------------------------------------------------- 1 | # EdgeLink:Rust 开发的 Node-RED 兼容运行时引擎 2 | [![Build Status]][actions] 3 | [![Releases](https://img.shields.io/github/release/oldrev/edgelink.svg)](https://github.com/oldrev/edgelink/releases) 4 | 5 | [Build Status]: https://img.shields.io/github/actions/workflow/status/oldrev/edgelink/CICD.yml?branch=master 6 | [actions]: https://github.com/oldrev/edgelink/actions?query=branch%3Amaster 7 | 8 | 9 | ![Node-RED Rust Backend](assets/banner.jpg) 10 | 11 | [English](README.md) | 简体中文 12 | 13 | ### 概述 14 | 15 | EdgeLink 是一个以 Rust 为底层语言开发的 [Node-RED](https://nodered.org/) 后端运行时引擎,旨在为 Node-RED 设计的 `flows.json` 流程提供高效的执行环境。EdgeLink 的设计聚焦于提高性能和降低内存消耗,使其能够顺利落地在 CPU 和内存资源受限的边缘计算设备中,从而实现从高性能桌面 PC 到边缘设备的全场景覆盖。 16 | 17 | 通过在高性能的桌面 PC 上复盘和测试工作流,用户可以将 EdgeLink 与 `flows.json` 工作流文件快速部署到资源有限的边缘计算设备中,实现关键路径的价值转化。 18 | 19 | ### 特性 20 | 21 | ![Memory Usage](assets/memory.png) 22 | 23 | - **高性能**: 通过 Rust 语言赋能,EdgeLink 在性能上发力,提供原生代码的执行速度,为复杂工作流的执行提供快速响应的能力。 24 | - **低内存占用**: EdgeLink 采用原生代码生态环境,与 Node-RED 的 NodeJS 平台对标,在内存使用上实现了显著的优化,极大地降低了系统的资源倾斜。测试表明,运行同一个简单的工作流,EdgeLink 仅消耗 Node-RED 10% 的物理内存。 25 | - **可扩展性**: 保持 Node-RED 的中台扩展性,通过插件化机制拉通自定义节点的开发。采用紧凑高效的 QuickJS Javascript 解释器为 `function` 节点的 Javascript 脚本提供支持,实现了从点到面的能力协同。 26 | - **尽量兼容 Node-RED**: 在工作流兼容性上尽力对齐 Node-RED 的现有工作流文件,允许用户复用 Node-RED 的设计器进行工作流的开发和测试。考虑到 Rust 是静态语言,Javascript 是动态语言,完全 100% 兼容存在挑战,但在多数场景中已实现较好的兼容性,确保了开发者心智的无缝过渡。 27 | 28 | EdgeLink 的设计和实现逻辑,力求通过精细化的资源管理和高效的执行模式,完善工作流在边缘计算设备上的布局,为用户提供一体化的解决方案。 29 | 30 | ## 快速开始 31 | 32 | ### 0. 安装 Node-RED 33 | 34 | 出于测试本项目的目的,我们首先需要安装 Node-RED 作为流程设计器,并生成 flows.json 文件。请参考 Node-RED 的文档获取安装和使用方法。 35 | 36 | 在 Node-RED 中完成流程设计后,请确保点击大红色的“Deploy”按钮,以生成 `flows.json` 文件。默认情况下,该文件位于 `~/.node-red/flows.json`。请注意不要使用本项目中尚未实现的 Node-RED 功能(功能实现状态请参考英文本文档)。 37 | 38 | ### 1. 构建 39 | 40 | ```bash 41 | cargo build -r 42 | ``` 43 | 44 | 45 | > [!IMPORTANT] 46 | > **Windows 用户请注意:** 47 | > 为了成功编译项目用到的 `rquickjs` 库,需要确保 `patch.exe` 程序存在于 `%PATH%` 环境变量中。`patch.exe` 用于为 QuickJS 库打上支持 Windows 的补丁,如果你已经安装了 Git,那 Git 都会附带 `patch.exe`。 48 | > 49 | > 你还需要安装 `rquickjs` 这个 crate 需要的 Microsoft Visual C++ 和 Windows SDK,推荐直接装 Visual Studio。 50 | 51 | 测试过的工具链(见 GitHub Actions): 52 | 53 | * `x86_64-pc-windows-msvc` 54 | * `x86_64-pc-windows-gnu` 55 | * `x86_64-unknown-linux-gnu` 56 | * `aarch64-unknown-linux-gnu` 57 | * `armv7-unknown-linux-gnueabihf` 58 | * `armv7-unknown-linux-gnueabi` 59 | 60 | ### 2. 运行 61 | 62 | ```bash 63 | cargo run -r 64 | ``` 65 | 66 | 或者 67 | 68 | ```bash 69 | ./target/release/edgelinkd 70 | ``` 71 | 72 | 在默认情况下,EdgeLink 将会读取 ~/.node-red/flows.json 并执行它。 73 | 74 | #### 运行单元测试 75 | 76 | ```bash 77 | cargo test --all 78 | ``` 79 | 80 | #### 运行集成测试 81 | 82 | 运行集成测试需要首先安装 Python 3.9+ 和对应的 Pytest 依赖库: 83 | 84 | ```bash 85 | pip install -r ./tests/requirements.txt 86 | ``` 87 | 88 | 然后执行以下命令即可: 89 | 90 | ```bash 91 | set PYO3_PYTHON=你的Python.exe路径 # 仅有 Windows 需要设置此环境变量 92 | cargo build --all 93 | py.test 94 | ``` 95 | 96 | 97 | ## 配置 98 | 99 | 在配置文件中可以调整各种设置,例如端口号、`flows.json` 文件位置等。请参考 [CONFIG.md](docs/CONFIG.md) 获取更多信息。 100 | 101 | ## 项目状态 102 | 103 | **Pre-Alpha**:项目当前处于发布前活跃开发阶段,不保证任何稳定性。 104 | 105 | 参考 [REDNODES-SPECS-DIFF.md](tests/REDNODES-SPECS-DIFF.md) 查看目前项目已实现节点和 Node-RED 的规格测试对比。 106 | 107 | ## 开发路线图 108 | 109 | 请参见项目的[里程碑页面](https://github.com/oldrev/edgelink/milestones)。 110 | 111 | ## 贡献 112 | 113 | ![Alt](https://repobeats.axiom.co/api/embed/cd18a784e88be20d79778703bda8858523c4257e.svg "Repobeats analytics image") 114 | 115 | 欢迎贡献!请阅读 [CONTRIBUTING.md](.github/CONTRIBUTING.md) 获取更多信息。 116 | 117 | 如果你想支持本项目的开发,可以考虑请我喝杯啤酒: 118 | 119 | [![爱发电支持](assets/aifadian.jpg)](https://afdian.com/a/mingshu) 120 | 121 | ## 反馈与技术支持 122 | 123 | 我们欢迎任何反馈!如果你遇到任何技术问题或者 bug,请提交 [issue](https://github.com/edge-link/edgelink/issues)。 124 | 125 | ### 社交网络聊天群: 126 | 127 | * [EdgeLink 开发交流 QQ 群:198723197](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=o3gEbpSHbFB6xjtC1Pm2mu0gZG62JNyr&authKey=D1qG9o0Nm%2FlDM8TQJXjr0aYluQ2TQp52wM9RDbNj83jzOy5OpCbHkwEI96SMMJxd&noverify=0&group_code=198723197) 128 | 129 | ### 联系作者 130 | 131 | - 邮箱:oldrev(at)gmail.com 132 | - QQ:55431671 133 | 134 | > 超巨型广告:没事儿时可以承接网站前端开发/管理系统开发/PCB 画板打样/单片机开发/压水晶头/中老年陪聊/工地打灰等软硬件项目。 135 | 136 | ## 许可证 137 | 138 | 此项目基于 Apache 2.0 许可证 - 详见 [LICENSE](LICENSE) 文件以获取更多详细信息。 139 | 140 | 版权所有©李维及其他贡献者,保留所有权利。 -------------------------------------------------------------------------------- /assets/aifadian.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldrev/edgelink/21aaed943fdc9ba14b4919ce6296cf4eb1713690/assets/aifadian.jpg -------------------------------------------------------------------------------- /assets/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldrev/edgelink/21aaed943fdc9ba14b4919ce6296cf4eb1713690/assets/banner.jpg -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldrev/edgelink/21aaed943fdc9ba14b4919ce6296cf4eb1713690/assets/logo.png -------------------------------------------------------------------------------- /assets/memory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldrev/edgelink/21aaed943fdc9ba14b4919ce6296cf4eb1713690/assets/memory.png -------------------------------------------------------------------------------- /assets/paypal_button.svg: -------------------------------------------------------------------------------- 1 | Support via PayPal -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs; 3 | use std::path::Path; 4 | use std::process::Command; 5 | 6 | fn main() { 7 | set_git_revision_hash(); 8 | check_patch(); 9 | gen_use_plugins_file(); 10 | } 11 | 12 | fn gen_use_plugins_file() { 13 | let out_dir = env::var_os("OUT_DIR").unwrap(); 14 | let dest_path = Path::new(&out_dir).join("__use_node_plugins.rs"); 15 | 16 | let plugins_dir = Path::new("node-plugins"); 17 | let mut plugin_names = Vec::new(); 18 | 19 | if plugins_dir.is_dir() { 20 | for entry in fs::read_dir(plugins_dir).unwrap() { 21 | let entry = entry.unwrap(); 22 | if entry.path().is_dir() { 23 | let plugin_name = entry.file_name().to_string_lossy().replace("-", "_"); 24 | plugin_names.push(plugin_name); 25 | } 26 | } 27 | } 28 | 29 | let mut file_content = String::new(); 30 | for plugin_name in plugin_names { 31 | file_content.push_str(&format!("extern crate {};\n", plugin_name)); 32 | } 33 | 34 | fs::write(&dest_path, file_content).unwrap(); 35 | 36 | println!("cargo:rerun-if-changed=node-plugins"); 37 | } 38 | 39 | /// Make the current git hash available to the build as the environment 40 | /// variable `EDGELINK_BUILD_GIT_HASH`. 41 | fn set_git_revision_hash() { 42 | let args = &["rev-parse", "--short=10", "HEAD"]; 43 | let Ok(output) = Command::new("git").args(args).output() else { 44 | return; 45 | }; 46 | let rev = String::from_utf8_lossy(&output.stdout).trim().to_string(); 47 | if rev.is_empty() { 48 | return; 49 | } 50 | println!("cargo:rustc-env=EDGELINK_BUILD_GIT_HASH={}", rev); 51 | } 52 | 53 | fn check_patch() { 54 | if env::consts::OS == "windows" { 55 | let output = Command::new("patch.exe") 56 | .arg("--version") 57 | .output() 58 | .expect("Failed to execute `patch.exe --version`, the GNU Patch program is required to build this project"); 59 | 60 | if output.status.success() { 61 | let version_info = String::from_utf8_lossy(&output.stdout); 62 | let first_line = version_info.lines().next().unwrap_or("Unknown version"); 63 | if !first_line.to_lowercase().contains("patch") { 64 | eprintln!("Error: The Patch program is required to build this project, but got: {}", first_line); 65 | std::process::exit(1); 66 | } 67 | } else { 68 | let error_info = String::from_utf8_lossy(&output.stderr); 69 | eprintln!("Error: Failed to get patch.exe version: {}", error_info); 70 | std::process::exit(1); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /crates/core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "edgelink-core" 3 | version = "0.1.0" 4 | edition = "2021" 5 | readme = "README.md" 6 | authors = ["Li Wei "] 7 | 8 | [lib] 9 | name = "edgelink_core" 10 | 11 | [dependencies] 12 | anyhow.workspace = true 13 | tokio = { workspace = true, features = [ 14 | "rt", 15 | "rt-multi-thread", 16 | "macros", 17 | "time", 18 | "fs", 19 | "net", 20 | "sync", 21 | "io-util", 22 | "io-std", 23 | ] } 24 | config.workspace = true 25 | async-trait.workspace = true 26 | log.workspace = true 27 | tokio-util.workspace = true 28 | thiserror.workspace = true 29 | nom.workspace = true 30 | bumpalo.workspace = true 31 | regex.workspace = true 32 | tokio-cron-scheduler.workspace = true 33 | chrono.workspace = true 34 | semver.workspace = true 35 | rquickjs = { optional = true, workspace = true } 36 | rquickjs-extra = { optional = true, workspace = true } 37 | #llrt_modules = { optional = true, workspace = true } 38 | rand.workspace = true 39 | base64.workspace = true 40 | # Serialization stuff 41 | bytes.workspace = true 42 | serde = { workspace = true, features = ["derive"] } 43 | serde_json.workspace = true 44 | bincode.workspace = true 45 | # Crates in this project 46 | edgelink-macro = { path = "../macro" } 47 | dashmap.workspace = true 48 | itertools.workspace = true 49 | smallvec.workspace = true 50 | smallstr.workspace = true 51 | inventory.workspace = true 52 | arrayvec = { workspace = true, features = ["std", "serde"] } 53 | 54 | [dev-dependencies] 55 | # Enable test-utilities in dev mode only. This is mostly for tests. 56 | tokio = { workspace = true, features = ["test-util"] } 57 | log4rs.workspace = true 58 | ctor.workspace = true 59 | 60 | 61 | [features] 62 | default = ["core", "js", "net"] 63 | core = [] 64 | pymod = [] 65 | #js = ["rquickjs", "rquickjs-extra", "llrt_modules"] 66 | js = ["rquickjs", "rquickjs-extra"] 67 | rqjs_bindgen = ["rquickjs/bindgen"] 68 | net = ["nodes_mqtt", "nodes_udp"] 69 | nodes_mqtt = [] 70 | nodes_http = ["tokio/net"] 71 | nodes_tcp = ["tokio/net"] 72 | nodes_udp = ["tokio/net"] 73 | nodes_websocket = [] 74 | -------------------------------------------------------------------------------- /crates/core/README.md: -------------------------------------------------------------------------------- 1 | # `edgelink_core` 2 | 3 | The core code for EdgeLink project. -------------------------------------------------------------------------------- /crates/core/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod runtime; 2 | pub mod text; 3 | pub mod utils; 4 | 5 | /// The `PluginRegistrar` is defined by the application and passed to `plugin_entry`. It's used 6 | /// for a plugin module to register itself with the application. 7 | pub trait PluginRegistrar { 8 | fn register_plugin(&mut self, plugin: Box); 9 | } 10 | 11 | /// `Plugin` is implemented by a plugin library for one or more types. As you need additional 12 | /// callbacks, they can be defined here. These are first class Rust trait objects, so you have the 13 | /// full flexibility of that system. The main thing you'll lose access to is generics, but that's 14 | /// expected with a plugin system 15 | pub trait Plugin { 16 | /// This is a callback routine implemented by the plugin. 17 | fn callback1(&self); 18 | /// Callbacks can take arguments and return values 19 | fn callback2(&self, i: i32) -> i32; 20 | } 21 | 22 | #[derive(thiserror::Error, Debug)] 23 | #[non_exhaustive] 24 | pub enum EdgelinkError { 25 | #[error("Permission Denied")] 26 | PermissionDenied, 27 | 28 | #[error("Invalid 'flows.json': {0}")] 29 | BadFlowsJson(String), 30 | 31 | #[error("Unsupported 'flows.json' format: {0}")] 32 | UnsupportedFlowsJsonFormat(String), 33 | 34 | #[error("Not supported: {0}")] 35 | NotSupported(String), 36 | 37 | #[error("Invalid arguments: {0}")] 38 | BadArgument(&'static str), 39 | 40 | #[error("Task cancelled")] 41 | TaskCancelled, 42 | 43 | #[error("{0}")] 44 | InvalidOperation(String), 45 | 46 | #[error("Out of range")] 47 | OutOfRange, 48 | 49 | #[error("Invalid configuration")] 50 | Configuration, 51 | 52 | #[error("Timed out")] 53 | Timeout, 54 | 55 | #[error("IO error")] 56 | Io(#[from] std::io::Error), 57 | 58 | #[error(transparent)] 59 | Other(#[from] crate::Error), // source and Display delegate to anyhow::Error 60 | } 61 | 62 | pub type Error = Box; 63 | 64 | pub type Result = anyhow::Result; 65 | 66 | pub use anyhow::Context as ErrorContext; 67 | 68 | impl EdgelinkError { 69 | pub fn invalid_operation(msg: &str) -> anyhow::Error { 70 | EdgelinkError::InvalidOperation(msg.into()).into() 71 | } 72 | } 73 | 74 | #[cfg(test)] 75 | mod tests { 76 | 77 | #[ctor::ctor] 78 | fn initialize_test_logger() { 79 | let stderr = log4rs::append::console::ConsoleAppender::builder() 80 | .target(log4rs::append::console::Target::Stdout) 81 | .encoder(Box::new(log4rs::encode::pattern::PatternEncoder::new("[{h({l})}]\t{m}{n}"))) 82 | .build(); 83 | 84 | let config = log4rs::Config::builder() 85 | .appender(log4rs::config::Appender::builder().build("stderr", Box::new(stderr))) 86 | .build(log4rs::config::Root::builder().appender("stderr").build(log::LevelFilter::Warn)) 87 | .unwrap(); 88 | 89 | let _ = log4rs::init_config(config).unwrap(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /crates/core/src/runtime/context/localfs.rs: -------------------------------------------------------------------------------- 1 | // .keepme 2 | -------------------------------------------------------------------------------- /crates/core/src/runtime/context/memory.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use async_trait::async_trait; 4 | use propex::PropexSegment; 5 | use tokio::sync::RwLock; 6 | 7 | use super::{EdgelinkError, ElementId, Variant}; 8 | use crate::runtime::context::*; 9 | use crate::Result; 10 | 11 | inventory::submit! { 12 | ProviderMetadata { type_: "memory", factory: MemoryContextStore::build } 13 | } 14 | 15 | struct MemoryContextStore { 16 | name: String, 17 | scopes: RwLock>, 18 | } 19 | 20 | impl MemoryContextStore { 21 | fn build(name: String, _options: Option<&ContextStoreOptions>) -> crate::Result> { 22 | let this = MemoryContextStore { name, scopes: RwLock::new(HashMap::new()) }; 23 | Ok(Box::new(this)) 24 | } 25 | } 26 | 27 | #[async_trait] 28 | impl ContextStore for MemoryContextStore { 29 | async fn name(&self) -> &str { 30 | &self.name 31 | } 32 | 33 | async fn open(&self) -> Result<()> { 34 | // No-op for in-memory store 35 | Ok(()) 36 | } 37 | 38 | async fn close(&self) -> Result<()> { 39 | // No-op for in-memory store 40 | Ok(()) 41 | } 42 | 43 | async fn get_one(&self, scope: &str, path: &[PropexSegment]) -> Result { 44 | let scopes = self.scopes.read().await; 45 | if let Some(scope_map) = scopes.get(scope) { 46 | if let Some(value) = scope_map.get_segs(path) { 47 | return Ok(value.clone()); 48 | } 49 | } 50 | Err(EdgelinkError::OutOfRange.into()) 51 | } 52 | 53 | async fn get_many(&self, scope: &str, keys: &[&str]) -> Result> { 54 | let scopes = self.scopes.read().await; 55 | if let Some(scope_map) = scopes.get(scope) { 56 | let mut result = Vec::new(); 57 | for key in keys { 58 | if let Some(value) = scope_map.get_nav(key, &[]) { 59 | result.push(value.clone()); 60 | } 61 | } 62 | return Ok(result); 63 | } 64 | Err(EdgelinkError::OutOfRange.into()) 65 | } 66 | 67 | async fn get_keys(&self, scope: &str) -> Result> { 68 | let scopes = self.scopes.read().await; 69 | if let Some(scope_map) = scopes.get(scope) { 70 | return Ok(scope_map.as_object().unwrap().keys().cloned().collect::>()); 71 | } 72 | Err(EdgelinkError::OutOfRange.into()) 73 | } 74 | 75 | async fn set_one(&self, scope: &str, path: &[PropexSegment], value: Variant) -> Result<()> { 76 | let mut scopes = self.scopes.write().await; 77 | let scope_map = scopes.entry(scope.to_string()).or_insert_with(Variant::empty_object); 78 | scope_map.set_segs_property(path, value, true)?; 79 | Ok(()) 80 | } 81 | 82 | async fn set_many(&self, scope: &str, pairs: Vec<(String, Variant)>) -> Result<()> { 83 | let mut scopes = self.scopes.write().await; 84 | let scope_map = scopes.entry(scope.to_string()).or_insert_with(Variant::empty_object); 85 | for (key, value) in pairs { 86 | let _ = scope_map.as_object_mut().unwrap().insert(key, value); 87 | } 88 | Ok(()) 89 | } 90 | 91 | async fn remove_one(&self, scope: &str, path: &[PropexSegment]) -> Result { 92 | let mut scopes = self.scopes.write().await; 93 | if let Some(scope_map) = scopes.get_mut(scope) { 94 | if let Some(value) = scope_map.as_object_mut().unwrap().remove_segs_property(path) { 95 | return Ok(value); 96 | } else { 97 | return Err(EdgelinkError::OutOfRange.into()); 98 | } 99 | } 100 | Err(EdgelinkError::OutOfRange.into()) 101 | } 102 | 103 | async fn delete(&self, scope: &str) -> Result<()> { 104 | let mut scopes = self.scopes.write().await; 105 | scopes.remove(scope); 106 | Ok(()) 107 | } 108 | 109 | async fn clean(&self, _active_nodes: &[ElementId]) -> Result<()> { 110 | /* 111 | let mut items = self.items.write().await; 112 | let scopes = active_nodes. scope.parse::(); 113 | items.retain(|scope, _| active_nodes.contains(&scope)); 114 | Ok(()) 115 | */ 116 | todo!() 117 | } 118 | } 119 | 120 | #[cfg(test)] 121 | mod tests { 122 | use super::MemoryContextStore; 123 | use crate::runtime::model::*; 124 | use serde_json::json; 125 | 126 | #[tokio::test] 127 | async fn test_it_should_store_property() { 128 | let context = MemoryContextStore::build("memory0".to_string(), None).unwrap(); 129 | 130 | assert!(context.get_one("nodeX", &propex::parse("foo").unwrap()).await.is_err()); 131 | assert!(context.set_one("nodeX", &propex::parse("foo").unwrap(), "test".into()).await.is_ok()); 132 | assert_eq!(context.get_one("nodeX", &propex::parse("foo").unwrap()).await.unwrap(), "test".into()); 133 | } 134 | 135 | #[tokio::test] 136 | async fn test_it_should_store_property_creates_parent_properties() { 137 | let context = MemoryContextStore::build("memory0".to_string(), None).unwrap(); 138 | 139 | context.set_one("nodeX", &propex::parse("foo.bar").unwrap(), "test".into()).await.unwrap(); 140 | 141 | assert_eq!( 142 | context.get_one("nodeX", &propex::parse("foo").unwrap()).await.unwrap(), 143 | json!({"bar": "test"}).into() 144 | ); 145 | } 146 | 147 | #[tokio::test] 148 | async fn test_it_should_delete_property() { 149 | let context = MemoryContextStore::build("memory0".to_string(), None).unwrap(); 150 | 151 | context.set_one("nodeX", &propex::parse("foo.abc.bar1").unwrap(), "test1".into()).await.unwrap(); 152 | 153 | context.set_one("nodeX", &propex::parse("foo.abc.bar2").unwrap(), "test2".into()).await.unwrap(); 154 | 155 | assert_eq!( 156 | context.get_one("nodeX", &propex::parse("foo.abc").unwrap()).await.unwrap(), 157 | json!({"bar1": "test1", "bar2": "test2"}).into() 158 | ); 159 | } 160 | 161 | #[tokio::test] 162 | async fn test_it_should_not_shared_context_with_other_scope() { 163 | let context = MemoryContextStore::build("memory0".to_string(), None).unwrap(); 164 | 165 | assert!(context.get_one("nodeX", &propex::parse("foo").unwrap()).await.is_err()); 166 | assert!(context.get_one("nodeY", &propex::parse("foo").unwrap()).await.is_err()); 167 | 168 | context.set_one("nodeX", &propex::parse("foo").unwrap(), "testX".into()).await.unwrap(); 169 | context.set_one("nodeY", &propex::parse("foo").unwrap(), "testY".into()).await.unwrap(); 170 | 171 | assert_eq!(context.get_one("nodeX", &propex::parse("foo").unwrap()).await.unwrap(), "testX".into()); 172 | assert_eq!(context.get_one("nodeY", &propex::parse("foo").unwrap()).await.unwrap(), "testY".into()); 173 | } 174 | } // tests 175 | -------------------------------------------------------------------------------- /crates/core/src/runtime/group.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::sync::Weak; 3 | 4 | use super::env::*; 5 | use super::flow::*; 6 | use super::model::json::*; 7 | use super::model::*; 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct Group { 11 | inner: Arc, 12 | } 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct WeakGroup { 16 | inner: Weak, 17 | } 18 | 19 | impl WeakGroup { 20 | pub fn upgrade(&self) -> Option { 21 | Weak::upgrade(&self.inner).map(|x| Group { inner: x }) 22 | } 23 | } 24 | 25 | impl FlowsElement for Group { 26 | fn id(&self) -> ElementId { 27 | self.inner.id 28 | } 29 | 30 | fn name(&self) -> &str { 31 | &self.inner.name 32 | } 33 | 34 | fn type_str(&self) -> &'static str { 35 | "group" 36 | } 37 | 38 | fn ordering(&self) -> usize { 39 | 0 40 | } 41 | 42 | fn parent_element(&self) -> Option { 43 | match self.inner.parent { 44 | GroupParent::Flow(ref flow) => flow.upgrade().map(|x| x.id()), 45 | GroupParent::Group(ref group) => group.upgrade().map(|x| x.id()), 46 | } 47 | } 48 | 49 | fn as_any(&self) -> &dyn ::std::any::Any { 50 | self 51 | } 52 | 53 | fn is_disabled(&self) -> bool { 54 | self.inner.disabled 55 | } 56 | 57 | fn get_path(&self) -> String { 58 | panic!("Group do not support path!") 59 | } 60 | } 61 | 62 | #[derive(Debug, Clone)] 63 | pub enum GroupParent { 64 | Flow(WeakFlow), 65 | Group(WeakGroup), 66 | } 67 | 68 | #[derive(Debug, Clone)] 69 | struct InnerGroup { 70 | pub id: ElementId, 71 | pub name: String, 72 | pub disabled: bool, 73 | pub parent: GroupParent, 74 | pub envs: Envs, 75 | } 76 | 77 | impl Group { 78 | pub fn downgrade(&self) -> WeakGroup { 79 | WeakGroup { inner: Arc::downgrade(&self.inner) } 80 | } 81 | 82 | pub(crate) fn new_flow_group(config: &RedGroupConfig, flow: &Flow) -> crate::Result { 83 | let envs_builder = EnvStoreBuilder::default().with_parent(flow.get_envs()); 84 | 85 | let inner = InnerGroup { 86 | id: config.id, 87 | name: config.name.clone(), 88 | disabled: config.disabled, 89 | parent: GroupParent::Flow(flow.downgrade()), 90 | envs: build_envs(envs_builder, config), 91 | }; 92 | Ok(Self { inner: Arc::new(inner) }) 93 | } 94 | 95 | pub(crate) fn new_subgroup(config: &RedGroupConfig, parent: &Group) -> crate::Result { 96 | let envs_builder = EnvStoreBuilder::default().with_parent(&parent.inner.envs); 97 | 98 | let inner = InnerGroup { 99 | id: config.id, 100 | name: config.name.clone(), 101 | disabled: config.disabled, 102 | parent: GroupParent::Group(parent.downgrade()), 103 | envs: build_envs(envs_builder, config), 104 | }; 105 | Ok(Self { inner: Arc::new(inner) }) 106 | } 107 | 108 | pub fn get_parent(&self) -> &GroupParent { 109 | &self.inner.parent 110 | } 111 | 112 | pub fn get_envs(&self) -> Envs { 113 | self.inner.envs.clone() 114 | } 115 | 116 | pub fn get_env(&self, key: &str) -> Option { 117 | self.inner.envs.evalute_env(key) 118 | } 119 | } 120 | 121 | fn build_envs(mut envs_builder: EnvStoreBuilder, config: &RedGroupConfig) -> Envs { 122 | if let Some(env_json) = config.rest.get("env") { 123 | envs_builder = envs_builder.load_json(env_json); 124 | } 125 | envs_builder 126 | .extends([ 127 | ("NR_GROUP_ID".into(), Variant::String(config.id.to_string())), 128 | ("NR_GROUP_NAME".into(), Variant::String(config.name.clone())), 129 | ]) 130 | .build() 131 | } 132 | -------------------------------------------------------------------------------- /crates/core/src/runtime/js/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "js")] 2 | pub mod util; 3 | -------------------------------------------------------------------------------- /crates/core/src/runtime/js/util.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use rquickjs::function::{Constructor, This}; 4 | use rquickjs::{Ctx, Function, IntoJs, Value}; 5 | 6 | pub fn deep_clone<'js>(ctx: Ctx<'js>, obj: Value<'js>) -> rquickjs::Result> { 7 | if let Some(obj_ref) = obj.as_object() { 8 | let globals = ctx.globals(); 9 | let date_ctor: Constructor = globals.get("Date")?; 10 | if obj_ref.is_instance_of(&date_ctor) { 11 | let get_time_fn: Function = obj_ref.get("getTime")?; 12 | let time: i64 = get_time_fn.call((This(&obj),))?; 13 | return date_ctor.construct((time,)); 14 | } 15 | 16 | if let Some(src_arr) = obj_ref.as_array() { 17 | let mut arr_copy = Vec::with_capacity(src_arr.len()); 18 | for item in src_arr.iter() { 19 | let cloned = deep_clone(ctx.clone(), item?)?; 20 | arr_copy.push(cloned); 21 | } 22 | return arr_copy.into_js(&ctx); 23 | } 24 | 25 | { 26 | let mut obj_copy: HashMap> = HashMap::with_capacity(obj_ref.len()); 27 | let has_own_property_fn: Function = obj_ref.get("hasOwnProperty")?; 28 | for item in obj_ref.props::>() { 29 | let (k, v) = item?; 30 | let has: bool = has_own_property_fn.call((This(&obj), k.as_str()))?; 31 | if has { 32 | obj_copy.insert(k, deep_clone(ctx.clone(), v)?); 33 | } 34 | } 35 | obj_copy.into_js(&ctx) 36 | } 37 | } else { 38 | Ok(obj) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /crates/core/src/runtime/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod context; 2 | pub mod engine; 3 | pub mod env; 4 | pub mod eval; 5 | pub mod flow; 6 | pub mod group; 7 | pub mod model; 8 | pub mod nodes; 9 | pub mod registry; 10 | pub mod subflow; 11 | 12 | #[cfg(feature = "js")] 13 | pub mod js; 14 | -------------------------------------------------------------------------------- /crates/core/src/runtime/model/eid.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::hash::Hash; 3 | use std::ops::BitXor; 4 | use std::str::FromStr; 5 | 6 | use crate::utils; 7 | use crate::*; 8 | 9 | #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] 10 | pub struct ElementId(u64); 11 | 12 | impl BitXor for ElementId { 13 | type Output = Self; 14 | 15 | fn bitxor(self, rhs: Self) -> Self::Output { 16 | ElementId(self.0 ^ rhs.0) 17 | } 18 | } 19 | 20 | impl fmt::Display for ElementId { 21 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 22 | write!(f, "{:016x}", self.0) 23 | } 24 | } 25 | 26 | impl Default for ElementId { 27 | fn default() -> Self { 28 | Self::empty() 29 | } 30 | } 31 | 32 | impl FromStr for ElementId { 33 | type Err = std::num::ParseIntError; 34 | 35 | fn from_str(s: &str) -> Result { 36 | Ok(ElementId(u64::from_str_radix(s, 16)?)) 37 | } 38 | } 39 | 40 | impl From for ElementId { 41 | fn from(value: u64) -> Self { 42 | ElementId(value) 43 | } 44 | } 45 | 46 | impl From for u64 { 47 | fn from(val: ElementId) -> Self { 48 | val.0 49 | } 50 | } 51 | 52 | impl ElementId { 53 | pub fn new() -> Self { 54 | Self(utils::generate_uid()) 55 | } 56 | 57 | pub fn empty() -> Self { 58 | Self(0) 59 | } 60 | 61 | pub fn is_empty(&self) -> bool { 62 | self.0 == 0 63 | } 64 | 65 | pub fn with_u64(id: u64) -> Self { 66 | Self(id) 67 | } 68 | 69 | pub fn to_chars(&self) -> [char; 16] { 70 | let hex_string = format!("{:016x}", self.0); // 格式化为16位十六进制字符串 71 | let mut char_array = ['0'; 16]; // 初始化一个字符数组 72 | for (i, c) in hex_string.chars().enumerate() { 73 | char_array[i] = c; // 填充字符数组 74 | } 75 | char_array 76 | } 77 | 78 | pub fn combine(lhs: &ElementId, rhs: &ElementId) -> crate::Result { 79 | if rhs.is_empty() { 80 | Err(crate::EdgelinkError::BadArgument("rhs").into()) 81 | } else if lhs.is_empty() { 82 | Err(crate::EdgelinkError::BadArgument("lhs").into()) 83 | } else { 84 | Ok(*lhs ^ *rhs) 85 | } 86 | } 87 | } 88 | 89 | impl serde::Serialize for ElementId { 90 | fn serialize(&self, serializer: S) -> Result 91 | where 92 | S: serde::Serializer, 93 | { 94 | let s = self.to_string(); 95 | serializer.serialize_str(&s) 96 | } 97 | } 98 | 99 | struct ElementIdVisitor; 100 | 101 | impl<'de> serde::de::Visitor<'de> for ElementIdVisitor { 102 | type Value = ElementId; 103 | 104 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 105 | formatter.write_str("a string representing an ElementId") 106 | } 107 | 108 | fn visit_str(self, value: &str) -> Result 109 | where 110 | E: serde::de::Error, 111 | { 112 | ElementId::from_str(value).map_err(|_| E::custom("failed to parse ElementId")) 113 | } 114 | } 115 | 116 | impl<'de> serde::Deserialize<'de> for ElementId { 117 | fn deserialize(deserializer: D) -> Result 118 | where 119 | D: serde::Deserializer<'de>, 120 | { 121 | deserializer.deserialize_str(ElementIdVisitor) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /crates/core/src/runtime/model/error.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::ElementId; 4 | 5 | #[derive(Debug, Clone, Serialize, Deserialize)] 6 | pub struct NodeErrorSource { 7 | pub id: ElementId, 8 | 9 | #[serde(rename = "type")] 10 | pub type_: String, 11 | 12 | pub name: String, 13 | 14 | pub count: usize, 15 | } 16 | 17 | #[derive(Debug, Clone, Serialize, Deserialize)] 18 | pub struct NodeError { 19 | pub message: String, 20 | 21 | pub source: Option, 22 | } 23 | -------------------------------------------------------------------------------- /crates/core/src/runtime/model/json/helpers.rs: -------------------------------------------------------------------------------- 1 | use crate::runtime::model::*; 2 | 3 | pub fn parse_red_id_str(id_str: &str) -> Option { 4 | id_str.parse().ok() 5 | } 6 | 7 | pub fn parse_red_id_value(id_value: &serde_json::Value) -> Option { 8 | id_value.as_str().and_then(|s| s.parse().ok()) 9 | } 10 | -------------------------------------------------------------------------------- /crates/core/src/runtime/model/json/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use crate::runtime::model::*; 4 | use serde_json::Value as JsonValue; 5 | 6 | pub mod deser; 7 | pub mod helpers; 8 | mod npdeser; 9 | 10 | #[derive(serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] 11 | pub struct RedPortConfig { 12 | pub node_ids: Vec, 13 | } 14 | 15 | #[derive(Debug, Clone, serde::Deserialize)] 16 | pub struct RedSubflowInstanceNodeType { 17 | pub type_name: String, 18 | pub subflow_id: ElementId, 19 | } 20 | 21 | #[derive(Debug, Clone, serde::Deserialize)] 22 | pub enum RedNodeType { 23 | Normal(String), 24 | SubflowInstance(RedSubflowInstanceNodeType), 25 | } 26 | 27 | #[derive(Debug, Clone, serde::Deserialize)] 28 | pub struct RedGroupConfig { 29 | #[serde(deserialize_with = "deser::deser_red_id")] 30 | pub id: ElementId, 31 | 32 | #[serde(default)] 33 | pub name: String, 34 | 35 | #[serde(default)] 36 | pub disabled: bool, 37 | 38 | #[serde(default, deserialize_with = "deser::deser_red_id_vec")] 39 | pub nodes: Vec, 40 | 41 | #[serde(deserialize_with = "deser::deser_red_id")] 42 | pub z: ElementId, 43 | 44 | #[serde(default, deserialize_with = "deser::deser_red_optional_id")] 45 | pub g: Option, 46 | 47 | #[serde(flatten)] 48 | pub rest: JsonValue, 49 | } 50 | 51 | #[derive(Debug, Clone, serde::Deserialize)] 52 | pub struct RedFlowConfig { 53 | #[serde(default)] 54 | pub disabled: bool, 55 | 56 | #[serde(deserialize_with = "deser::deser_red_id")] 57 | pub id: ElementId, 58 | 59 | #[serde(default)] 60 | pub info: String, 61 | 62 | #[serde(default)] 63 | pub label: String, 64 | 65 | #[serde(alias = "type")] 66 | pub type_name: String, 67 | 68 | #[serde(skip)] 69 | pub nodes: Vec, 70 | 71 | #[serde(skip)] 72 | pub groups: Vec, 73 | 74 | #[serde(default, alias = "in")] 75 | pub in_ports: Vec, 76 | 77 | #[serde(default, alias = "out")] 78 | pub out_ports: Vec, 79 | 80 | #[serde(skip)] 81 | pub subflow_node_id: Option, 82 | 83 | #[serde(skip, default)] 84 | pub ordering: usize, 85 | 86 | #[serde(flatten)] 87 | pub rest: JsonValue, 88 | } 89 | 90 | #[derive(Debug, Clone, serde::Deserialize)] 91 | pub struct RedFlowNodeConfig { 92 | #[serde(deserialize_with = "deser::deser_red_id")] 93 | pub id: ElementId, 94 | 95 | #[serde(alias = "type")] 96 | pub type_name: String, 97 | 98 | #[serde(default)] 99 | pub name: String, 100 | 101 | #[serde(deserialize_with = "deser::deser_red_id")] 102 | pub z: ElementId, 103 | 104 | #[serde(default, deserialize_with = "deser::deser_red_optional_id")] 105 | pub g: Option, 106 | 107 | #[serde(default)] 108 | pub active: Option, 109 | 110 | #[serde(default, alias = "d")] 111 | pub disabled: bool, 112 | 113 | #[serde(default, deserialize_with = "deser::deserialize_wires")] 114 | pub wires: Vec, 115 | 116 | #[serde(skip, default)] 117 | pub ordering: usize, 118 | 119 | #[serde(flatten)] 120 | pub rest: JsonValue, 121 | } 122 | 123 | #[derive(Debug, Clone, serde::Deserialize)] 124 | pub struct RedGlobalNodeConfig { 125 | #[serde(deserialize_with = "deser::deser_red_id")] 126 | pub id: ElementId, 127 | 128 | #[serde(alias = "type")] 129 | pub type_name: String, 130 | 131 | #[serde(default)] 132 | pub name: String, 133 | 134 | #[serde(default)] 135 | pub active: Option, 136 | 137 | #[serde(default)] 138 | pub disabled: bool, 139 | 140 | #[serde(skip, default)] 141 | pub ordering: usize, 142 | 143 | #[serde(flatten)] 144 | pub rest: JsonValue, 145 | } 146 | 147 | #[derive(Debug, Clone, serde::Deserialize)] 148 | pub struct RedSubflowPortWire { 149 | #[serde(deserialize_with = "deser::deser_red_id")] 150 | pub id: ElementId, 151 | 152 | #[serde(default)] 153 | pub port: usize, 154 | } 155 | 156 | #[derive(Debug, Clone, serde::Deserialize)] 157 | pub struct RedSubflowPort { 158 | // x: i32, 159 | // y: i32, 160 | #[serde(default)] 161 | pub wires: Vec, 162 | } 163 | 164 | #[derive(Debug, Clone)] 165 | pub struct ResolvedFlows { 166 | pub flows: Vec, 167 | pub global_nodes: Vec, 168 | } 169 | 170 | impl Display for RedFlowNodeConfig { 171 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 172 | write!(f, "NodeJSON(id='{}', name='{}', type='{}')", self.id, self.name, self.type_name) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /crates/core/src/runtime/model/json/npdeser.rs: -------------------------------------------------------------------------------- 1 | //! [npdeser] this mod use to de deserializer node logic properties transport to real logic. 2 | //! 3 | //! # example 4 | //! > this appendNewline config is belong to node red core node [file], Used to determine whether 5 | //! > to wrap a file ,it's could It should be a boolean type, but the code logic allows it to be 6 | //! > any non undefined, true false 0 and 1, and any character ,and any str. so need this mod handle 7 | //! > this scene 8 | //! ```js 9 | //! this.appendNewline = n.appendNewline; 10 | //! 11 | //! if ((node.appendNewline) && (!Buffer.isBuffer(data)) && aflg) { data += os.EOL; } 12 | //! ``` 13 | //! 14 | #![allow(dead_code)] 15 | use serde::de::{Error, Unexpected, Visitor}; 16 | use serde::{de, Deserializer}; 17 | use std::str; 18 | 19 | pub fn deser_bool_in_if_condition<'de, D>(deserializer: D) -> Result 20 | where 21 | D: Deserializer<'de>, 22 | { 23 | struct BoolVisitor; 24 | 25 | impl<'de> de::Visitor<'de> for BoolVisitor { 26 | type Value = bool; 27 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 28 | formatter.write_str("a bool, convert failed") 29 | } 30 | fn visit_bytes(self, v: &[u8]) -> Result 31 | where 32 | E: Error, 33 | { 34 | match str::from_utf8(v) { 35 | Ok(s) => { 36 | if let Some(value) = Self::convert_to_bool(s) { 37 | return value; 38 | } 39 | Ok(true) 40 | } 41 | Err(_) => Err(Error::invalid_value(Unexpected::Bool(false), &self)), 42 | } 43 | } 44 | fn visit_u64(self, v: u64) -> Result 45 | where 46 | E: Error, 47 | { 48 | if v == 0 { 49 | return Ok(false); 50 | } 51 | Ok(true) 52 | } 53 | fn visit_i64(self, v: i64) -> Result 54 | where 55 | E: Error, 56 | { 57 | if v == 0 { 58 | return Ok(false); 59 | } 60 | Ok(true) 61 | } 62 | fn visit_u32(self, v: u32) -> Result 63 | where 64 | E: Error, 65 | { 66 | if v == 0 { 67 | return Ok(false); 68 | } 69 | Ok(true) 70 | } 71 | fn visit_i32(self, v: i32) -> Result 72 | where 73 | E: Error, 74 | { 75 | if v == 0 { 76 | return Ok(false); 77 | } 78 | Ok(true) 79 | } 80 | 81 | fn visit_f64(self, v: f64) -> Result 82 | where 83 | E: Error, 84 | { 85 | if v == 0.0 { 86 | return Ok(false); 87 | } 88 | Ok(true) 89 | } 90 | 91 | fn visit_f32(self, v: f32) -> Result 92 | where 93 | E: Error, 94 | { 95 | if v == 0.0 { 96 | return Ok(false); 97 | } 98 | Ok(true) 99 | } 100 | 101 | fn visit_str(self, v: &str) -> Result 102 | where 103 | E: Error, 104 | { 105 | if let Some(value) = Self::convert_to_bool(v) { 106 | return value; 107 | } 108 | Ok(true) 109 | } 110 | } 111 | 112 | impl BoolVisitor { 113 | fn convert_to_bool(v: &str) -> Option::Value, E>> 114 | where 115 | E: Error, 116 | { 117 | if v.is_empty() || v == "0" || v.contains("false") || v.contains("False") || v.contains("FALSE") { 118 | return Some(Ok(false)); 119 | } 120 | None 121 | } 122 | } 123 | 124 | deserializer.deserialize_any(BoolVisitor) 125 | } 126 | 127 | #[cfg(test)] 128 | mod tests { 129 | use super::*; 130 | use serde::Deserialize; 131 | use serde_json::json; 132 | 133 | #[derive(Deserialize, Debug)] 134 | struct TestNodeConfig { 135 | #[serde(deserialize_with = "crate::runtime::model::json::npdeser::deser_bool_in_if_condition")] 136 | test: bool, 137 | } 138 | 139 | #[test] 140 | fn test_deser_bool_in_if_condition() { 141 | let value_str = json!({"test":"xxx"}); 142 | let result = TestNodeConfig::deserialize(value_str).unwrap(); 143 | assert!(result.test); 144 | 145 | let value_str = json!({"test":"true"}); 146 | let result = TestNodeConfig::deserialize(value_str).unwrap(); 147 | assert!(result.test); 148 | 149 | let value_str = json!({"test":"false"}); 150 | let result = TestNodeConfig::deserialize(value_str).unwrap(); 151 | assert!(!result.test); 152 | 153 | let value_str = json!({"test":"False"}); 154 | let result = TestNodeConfig::deserialize(value_str).unwrap(); 155 | assert!(!result.test); 156 | 157 | let value_str = json!({"test":"0"}); 158 | let result = TestNodeConfig::deserialize(value_str).unwrap(); 159 | assert!(!result.test); 160 | 161 | let value_str = json!({"test":1.0}); 162 | let result = TestNodeConfig::deserialize(value_str).unwrap(); 163 | assert!(result.test); 164 | 165 | let value_str = json!({"test":0.0}); 166 | let result = TestNodeConfig::deserialize(value_str).unwrap(); 167 | assert!(!result.test); 168 | 169 | let value_str = json!({"test":0}); 170 | let result = TestNodeConfig::deserialize(value_str).unwrap(); 171 | assert!(!result.test); 172 | 173 | let value_str = json!({"test":1}); 174 | let result = TestNodeConfig::deserialize(value_str).unwrap(); 175 | assert!(result.test); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /crates/core/src/runtime/model/mod.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | use std::sync::Arc; 3 | use tokio::sync::Mutex; 4 | use tokio_util::sync::CancellationToken; 5 | 6 | use tokio; 7 | use tokio::sync::mpsc; 8 | 9 | use crate::runtime::nodes::FlowNodeBehavior; 10 | use crate::EdgelinkError; 11 | 12 | mod eid; 13 | mod error; 14 | mod msg; 15 | mod red_types; 16 | mod settings; 17 | mod variant; 18 | 19 | pub mod json; 20 | pub mod propex; 21 | 22 | pub use eid::*; 23 | pub use error::*; 24 | pub use msg::*; 25 | pub use red_types::*; 26 | pub use settings::*; 27 | pub use variant::*; 28 | 29 | use super::context::Context; 30 | use super::flow::Flow; 31 | 32 | pub trait FlowsElement: Sync + Send { 33 | fn id(&self) -> ElementId; 34 | fn name(&self) -> &str; 35 | fn type_str(&self) -> &'static str; 36 | fn ordering(&self) -> usize; 37 | fn is_disabled(&self) -> bool; 38 | fn as_any(&self) -> &dyn ::std::any::Any; 39 | fn parent_element(&self) -> Option; 40 | fn get_path(&self) -> String; 41 | } 42 | 43 | pub trait ContextHolder: FlowsElement + Sync + Send { 44 | fn context(&self) -> Arc; 45 | } 46 | 47 | #[derive(Debug)] 48 | pub struct PortWire { 49 | // pub target_node_id: ElementId, 50 | // pub target_node: Weak, 51 | pub msg_sender: tokio::sync::mpsc::Sender, 52 | } 53 | 54 | impl PortWire { 55 | pub async fn tx(&self, msg: MsgHandle, cancel: CancellationToken) -> crate::Result<()> { 56 | tokio::select! { 57 | 58 | send_result = self.msg_sender.send(msg) => send_result.map_err(|e| 59 | crate::EdgelinkError::InvalidOperation(format!("Failed to transmit message: {}", e)).into()), 60 | 61 | _ = cancel.cancelled() => 62 | Err(crate::EdgelinkError::TaskCancelled.into()), 63 | } 64 | } 65 | } 66 | 67 | #[derive(Debug)] 68 | pub struct Port { 69 | pub wires: Vec, 70 | } 71 | 72 | impl Port { 73 | pub fn empty() -> Self { 74 | Port { wires: Vec::new() } 75 | } 76 | } 77 | 78 | pub type MsgSender = mpsc::Sender; 79 | pub type MsgReceiver = mpsc::Receiver; 80 | 81 | #[derive(Debug)] 82 | pub struct MsgReceiverHolder { 83 | pub rx: Mutex, 84 | } 85 | 86 | impl MsgReceiverHolder { 87 | pub fn new(rx: MsgReceiver) -> Self { 88 | MsgReceiverHolder { rx: Mutex::new(rx) } 89 | } 90 | 91 | pub async fn recv_msg_forever(&self) -> crate::Result { 92 | let rx = &mut self.rx.lock().await; 93 | match rx.recv().await { 94 | Some(msg) => Ok(msg), 95 | None => { 96 | log::error!("Failed to receive message"); 97 | Err(EdgelinkError::InvalidOperation("No message in the bounded channel!".to_string()).into()) 98 | } 99 | } 100 | } 101 | 102 | pub async fn recv_msg(&self, stop_token: CancellationToken) -> crate::Result { 103 | tokio::select! { 104 | result = self.recv_msg_forever() => { 105 | result 106 | } 107 | 108 | _ = stop_token.cancelled() => { 109 | // The token was cancelled 110 | Err(EdgelinkError::TaskCancelled.into()) 111 | } 112 | } 113 | } 114 | } 115 | 116 | pub type MsgUnboundedSender = mpsc::UnboundedSender; 117 | pub type MsgUnboundedReceiver = mpsc::UnboundedReceiver; 118 | 119 | #[derive(Debug)] 120 | pub struct MsgUnboundedReceiverHolder { 121 | pub rx: Mutex, 122 | } 123 | 124 | impl MsgUnboundedReceiverHolder { 125 | pub fn new(rx: MsgUnboundedReceiver) -> Self { 126 | MsgUnboundedReceiverHolder { rx: Mutex::new(rx) } 127 | } 128 | 129 | pub async fn recv_msg_forever(&self) -> crate::Result { 130 | let rx = &mut self.rx.lock().await; 131 | match rx.recv().await { 132 | Some(msg) => Ok(msg), 133 | None => { 134 | log::error!("Failed to receive message"); 135 | Err(EdgelinkError::InvalidOperation("No message in the unbounded channel!".to_string()).into()) 136 | } 137 | } 138 | } 139 | 140 | pub async fn recv_msg(&self, stop_token: CancellationToken) -> crate::Result { 141 | tokio::select! { 142 | result = self.recv_msg_forever() => { 143 | result 144 | } 145 | 146 | _ = stop_token.cancelled() => { 147 | // The token was cancelled 148 | Err(EdgelinkError::TaskCancelled.into()) 149 | } 150 | } 151 | } 152 | } 153 | 154 | pub trait SettingHolder { 155 | fn get_setting<'a>(name: &'a str, node: Option<&'a dyn FlowNodeBehavior>, flow: Option<&'a Flow>) -> &'a Variant; 156 | } 157 | 158 | pub trait RuntimeElement: Any { 159 | fn as_any(&self) -> &dyn Any; 160 | } 161 | 162 | impl RuntimeElement for T { 163 | fn as_any(&self) -> &dyn Any { 164 | self 165 | } 166 | } 167 | 168 | pub fn query_trait(ele: &T) -> Option<&U> { 169 | ele.as_any().downcast_ref::() 170 | } 171 | 172 | pub type MsgEventSender = tokio::sync::broadcast::Sender; 173 | pub type MsgEventReceiver = tokio::sync::broadcast::Receiver; 174 | -------------------------------------------------------------------------------- /crates/core/src/runtime/model/red_types.rs: -------------------------------------------------------------------------------- 1 | use crate::runtime::model::*; 2 | use serde; 3 | 4 | pub struct RedTypeValue<'a> { 5 | pub red_type: &'a str, 6 | pub id: Option, 7 | } 8 | 9 | #[derive(Debug, Default, Clone, Copy, PartialEq, serde::Deserialize, PartialOrd)] 10 | pub enum RedPropertyType { 11 | #[serde(rename = "str")] 12 | #[default] 13 | Str, 14 | 15 | #[serde(rename = "num")] 16 | Num, 17 | 18 | #[serde(rename = "json")] 19 | Json, 20 | 21 | #[serde(rename = "re")] 22 | Re, 23 | 24 | #[serde(rename = "date")] 25 | Date, 26 | 27 | #[serde(rename = "bin")] 28 | Bin, 29 | 30 | #[serde(rename = "msg")] 31 | Msg, 32 | 33 | #[serde(rename = "flow")] 34 | Flow, 35 | 36 | #[serde(rename = "global")] 37 | Global, 38 | 39 | #[serde(rename = "bool")] 40 | Bool, 41 | 42 | #[serde(rename = "jsonata")] 43 | Jsonata, 44 | 45 | #[serde(rename = "env")] 46 | Env, 47 | } 48 | 49 | impl RedPropertyType { 50 | pub fn is_constant(&self) -> bool { 51 | matches!( 52 | self, 53 | RedPropertyType::Str 54 | | RedPropertyType::Num 55 | | RedPropertyType::Json 56 | | RedPropertyType::Bin 57 | | RedPropertyType::Bool 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /crates/core/src/runtime/model/settings.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Debug, Clone, Deserialize)] 4 | pub struct NodeSettings { 5 | pub msg_queue_capacity: usize, 6 | } 7 | 8 | #[derive(Debug, Clone, Deserialize)] 9 | pub enum RunEnv { 10 | #[serde(rename = "dev")] 11 | Development, 12 | 13 | #[serde(rename = "prod")] 14 | Production, 15 | } 16 | 17 | #[derive(Debug, Clone, Deserialize)] 18 | pub struct EngineSettings { 19 | pub run_env: RunEnv, 20 | pub node: NodeSettings, 21 | } 22 | -------------------------------------------------------------------------------- /crates/core/src/runtime/model/variant/array.rs: -------------------------------------------------------------------------------- 1 | //use std::borrow::Cow; 2 | 3 | use super::*; 4 | 5 | //pub type VariantObjectMap = BTreeMap; 6 | 7 | pub type VariantArray = Vec; 8 | -------------------------------------------------------------------------------- /crates/core/src/runtime/model/variant/converts.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use super::*; 4 | 5 | impl From<&Variant> for String { 6 | #[inline] 7 | fn from(var: &Variant) -> Self { 8 | match var { 9 | Variant::Number(f) => f.to_string(), 10 | Variant::String(s) => s.clone(), 11 | Variant::Regexp(s) => s.to_string(), 12 | Variant::Bool(b) => b.to_string(), 13 | Variant::Date(d) => { 14 | let dt_now_utc: chrono::DateTime = (*d).into(); 15 | dt_now_utc.to_string() 16 | } 17 | _ => "".to_string(), 18 | } 19 | } 20 | } 21 | 22 | macro_rules! implfrom { 23 | ($($v:ident($t:ty)),+ $(,)?) => { 24 | $( 25 | impl From<$t> for Variant { 26 | #[inline] 27 | fn from(value: $t) -> Self { 28 | Self::$v(value.into()) 29 | } 30 | } 31 | )+ 32 | }; 33 | } 34 | 35 | implfrom! { 36 | Bytes(Vec), 37 | 38 | String(String), 39 | String(&str), 40 | 41 | Bool(bool), 42 | 43 | Array(&[Variant]), 44 | Array(Vec), 45 | 46 | // Object(&[(String, Variant)]), 47 | // Object(&[(&str, Variant)]), 48 | Object(VariantObjectMap), 49 | // Object(&BTreeMap), 50 | // Object(BTreeMap<&str, Variant>), 51 | } 52 | 53 | impl From for Variant { 54 | fn from(f: f32) -> Self { 55 | serde_json::Number::from_f64(f as f64).map_or(Variant::Null, Variant::Number) 56 | } 57 | } 58 | 59 | impl From for Variant { 60 | fn from(f: f64) -> Self { 61 | serde_json::Number::from_f64(f).map_or(Variant::Null, Variant::Number) 62 | } 63 | } 64 | 65 | impl From for Variant { 66 | fn from(f: i32) -> Self { 67 | Variant::Number(serde_json::Number::from(f as i64)) 68 | } 69 | } 70 | 71 | impl From for Variant { 72 | fn from(f: u32) -> Self { 73 | Variant::Number(serde_json::Number::from(f as u64)) 74 | } 75 | } 76 | 77 | impl From for Variant { 78 | fn from(f: i64) -> Self { 79 | Variant::Number(serde_json::Number::from(f)) 80 | } 81 | } 82 | 83 | impl From for Variant { 84 | fn from(f: u64) -> Self { 85 | Variant::Number(serde_json::Number::from(f)) 86 | } 87 | } 88 | 89 | impl From for Variant { 90 | #[inline] 91 | fn from(value: char) -> Self { 92 | Variant::String(value.to_string()) 93 | } 94 | } 95 | 96 | impl From<&[(String, Variant)]> for Variant { 97 | #[inline] 98 | fn from(value: &[(String, Variant)]) -> Self { 99 | let map: VariantObjectMap = value.iter().map(|x| (x.0.clone(), x.1.clone())).collect(); 100 | Variant::Object(map) 101 | } 102 | } 103 | 104 | impl From<[(&str, Variant); N]> for Variant { 105 | #[inline] 106 | fn from(value: [(&str, Variant); N]) -> Self { 107 | let map: VariantObjectMap = value.iter().map(|x| (x.0.to_string(), x.1.clone())).collect(); 108 | Variant::Object(map) 109 | } 110 | } 111 | 112 | impl From<&[u8]> for Variant { 113 | fn from(array: &[u8]) -> Self { 114 | Variant::Bytes(array.to_vec()) 115 | } 116 | } 117 | 118 | impl<'a> From> for Variant { 119 | fn from(f: Cow<'a, str>) -> Self { 120 | Variant::String(f.into_owned()) 121 | } 122 | } 123 | 124 | impl From for Variant { 125 | fn from(f: serde_json::Number) -> Self { 126 | Variant::Number(f) 127 | } 128 | } 129 | 130 | impl From<()> for Variant { 131 | fn from((): ()) -> Self { 132 | Variant::Null 133 | } 134 | } 135 | 136 | impl From> for Variant 137 | where 138 | T: Into, 139 | { 140 | fn from(opt: Option) -> Self { 141 | match opt { 142 | None => Variant::Null, 143 | Some(value) => Into::into(value), 144 | } 145 | } 146 | } 147 | 148 | impl From for Variant { 149 | fn from(jv: serde_json::Value) -> Self { 150 | match jv { 151 | serde_json::Value::Null => Variant::Null, 152 | serde_json::Value::Bool(boolean) => Variant::from(boolean), 153 | serde_json::Value::Number(number) => Variant::Number(number), 154 | serde_json::Value::String(string) => Variant::String(string.to_owned()), 155 | serde_json::Value::Array(array) => Variant::Array(array.iter().map(Variant::from).collect()), 156 | serde_json::Value::Object(object) => { 157 | let new_map: VariantObjectMap = object.iter().map(|(k, v)| (k.to_owned(), Variant::from(v))).collect(); 158 | Variant::Object(new_map) 159 | } 160 | } 161 | } 162 | } 163 | 164 | impl From<&serde_json::Value> for Variant { 165 | fn from(jv: &serde_json::Value) -> Self { 166 | match jv { 167 | serde_json::Value::Null => Variant::Null, 168 | serde_json::Value::Bool(boolean) => Variant::from(*boolean), 169 | serde_json::Value::Number(number) => Variant::Number(number.clone()), 170 | serde_json::Value::String(string) => Variant::String(string.clone()), 171 | serde_json::Value::Array(array) => Variant::Array(array.iter().map(Variant::from).collect()), 172 | serde_json::Value::Object(object) => { 173 | let new_map: VariantObjectMap = object.iter().map(|(k, v)| (k.clone(), Variant::from(v))).collect(); 174 | Variant::Object(new_map) 175 | } 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /crates/core/src/runtime/model/variant/js_support.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[cfg(feature = "js")] 4 | mod js { 5 | pub use rquickjs::*; 6 | } 7 | 8 | #[cfg(feature = "js")] 9 | impl<'js> js::FromJs<'js> for Variant { 10 | fn from_js(_ctx: &js::Ctx<'js>, jv: js::Value<'js>) -> js::Result { 11 | match jv.type_of() { 12 | js::Type::Undefined => Ok(Variant::Null), 13 | 14 | js::Type::Null => Ok(Variant::Null), 15 | 16 | js::Type::Bool => Ok(Variant::Bool(jv.get()?)), 17 | 18 | js::Type::Int => Ok(Variant::from(jv.get::()?)), 19 | 20 | js::Type::Float => Ok(Variant::from(jv.get::()?)), 21 | 22 | js::Type::String => Ok(Variant::String(jv.get()?)), 23 | 24 | js::Type::Symbol => Ok(Variant::String(jv.get()?)), 25 | 26 | js::Type::Array => { 27 | if let Some(arr) = jv.as_array() { 28 | if let Some(buf) = arr.as_typed_array::() { 29 | match buf.as_bytes() { 30 | Some(bytes) => Ok(Variant::Bytes(bytes.to_vec())), 31 | None => { 32 | Err(js::Error::FromJs { from: "TypedArray", to: "Variant::Bytes", message: None }) 33 | } 34 | } 35 | } else { 36 | let mut vec: Vec = Vec::with_capacity(arr.len()); 37 | for item in arr.iter() { 38 | match item { 39 | Ok(v) => vec.push(Variant::from_js(_ctx, v)?), 40 | Err(err) => { 41 | return Err(err); 42 | } 43 | } 44 | } 45 | Ok(Variant::Array(vec)) 46 | } 47 | } else { 48 | Ok(Variant::Null) 49 | } 50 | } 51 | 52 | js::Type::Object => { 53 | if let Some(jo) = jv.as_object() { 54 | let global = _ctx.globals(); 55 | let date_ctor: Constructor = global.get("Date")?; 56 | let regexp_ctor: Constructor = global.get("RegExp")?; 57 | if jo.is_instance_of(date_ctor) { 58 | let st = jv.get::()?; 59 | Ok(Variant::Date(st)) 60 | } else if jo.is_instance_of(regexp_ctor) { 61 | let to_string_fn: js::Function = jo.get("toString")?; 62 | let re_str: String = to_string_fn.call((js::function::This(jv),))?; 63 | match Regex::new(re_str.as_str()) { 64 | Ok(re) => Ok(Variant::Regexp(re)), 65 | Err(_) => Err(js::Error::FromJs { 66 | from: "JS object", 67 | to: "Variant::Regexp", 68 | message: Some(format!("Failed to create Regex from: '{}'", re_str)), 69 | }), 70 | } 71 | } else if let Some(buf) = jo.as_array_buffer() { 72 | match buf.as_bytes() { 73 | Some(bytes) => Ok(Variant::Bytes(bytes.to_vec())), 74 | None => Err(js::Error::FromJs { from: "ArrayBuffer", to: "Variant::Bytes", message: None }), 75 | } 76 | } else { 77 | let mut map = VariantObjectMap::new(); 78 | for result in jo.props::() { 79 | match result { 80 | Ok((ref k, v)) => { 81 | map.insert(k.clone(), Variant::from_js(_ctx, v)?); 82 | } 83 | Err(e) => { 84 | log::error!("Unknown fatal error: {}", e); 85 | unreachable!(); 86 | } 87 | } 88 | } 89 | Ok(Variant::Object(map)) 90 | } 91 | } else { 92 | Err(js::Error::FromJs { from: "JS object", to: "Variant::Object", message: None }) 93 | } 94 | } 95 | 96 | _ => Err(js::Error::FromJs { from: "Unknown JS type", to: "", message: None }), 97 | } 98 | } 99 | } 100 | 101 | #[cfg(feature = "js")] 102 | impl<'js> js::IntoJs<'js> for Variant { 103 | fn into_js(self, ctx: &js::Ctx<'js>) -> js::Result> { 104 | use js::function::Constructor; 105 | 106 | match self { 107 | Variant::Array(arr) => arr.into_js(ctx), 108 | 109 | Variant::Bool(b) => b.into_js(ctx), 110 | 111 | Variant::Bytes(bytes) => Ok(js::ArrayBuffer::new(ctx.clone(), bytes)?.into_value()), 112 | 113 | Variant::Number(num) => { 114 | if let Some(f) = num.as_f64() { 115 | f.into_js(ctx) 116 | } else if let Some(i) = num.as_i64() { 117 | i.into_js(ctx) 118 | } else if let Some(u) = num.as_u64() { 119 | u.into_js(ctx) 120 | } else { 121 | unreachable!(); 122 | } 123 | } 124 | 125 | Variant::Null => Ok(js::Value::new_null(ctx.clone())), 126 | 127 | Variant::Object(map) => map.into_js(ctx), 128 | 129 | Variant::String(s) => s.into_js(ctx), 130 | 131 | Variant::Date(t) => t.into_js(ctx), 132 | 133 | Variant::Regexp(re) => { 134 | let global = ctx.globals(); 135 | let regexp_ctor: Constructor = global.get("RegExp")?; 136 | regexp_ctor.construct((re.as_str(),)) 137 | } 138 | } 139 | } 140 | } 141 | 142 | #[cfg(feature = "js")] 143 | impl<'js> js::IntoJs<'js> for UndefinableVariant { 144 | fn into_js(self, ctx: &js::Ctx<'js>) -> js::Result> { 145 | match self.0 { 146 | Some(var) => var.into_js(ctx), 147 | None => Ok(js::Value::new_undefined(ctx.clone())), 148 | } 149 | } 150 | } 151 | 152 | #[cfg(test)] 153 | mod tests { 154 | use js::IntoJs; 155 | use serde_json::*; 156 | 157 | use super::*; 158 | 159 | #[test] 160 | fn variant_into_js() { 161 | let js_rt = js::Runtime::new().unwrap(); 162 | let ctx = js::Context::full(&js_rt).unwrap(); 163 | 164 | let foo = Variant::from(json!({ 165 | "intValue": 123, 166 | "strValue": "hello", 167 | "arrayValue": [1, 2, 3], 168 | })); 169 | 170 | ctx.with(|ctx| { 171 | let globs = ctx.globals(); 172 | globs.set("foo", foo.into_js(&ctx).unwrap()).unwrap(); 173 | 174 | let v: i64 = ctx.eval("foo.intValue").unwrap(); 175 | assert_eq!(v, 123); 176 | 177 | let v: std::string::String = ctx.eval("foo.strValue").unwrap(); 178 | assert_eq!(v, "hello".to_string()); 179 | 180 | let v: Vec = ctx.eval("foo.arrayValue").unwrap(); 181 | assert_eq!(v, vec![1, 2, 3]); 182 | 183 | let v: Vec = ctx.eval("foo.arrayValue").unwrap(); 184 | assert_eq!(v, vec![Variant::from(1), Variant::from(2), Variant::from(3)]); 185 | }); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /crates/core/src/runtime/model/variant/ser.rs: -------------------------------------------------------------------------------- 1 | use serde::ser::Serialize; 2 | 3 | use super::*; 4 | 5 | impl Serialize for Variant { 6 | fn serialize(&self, serializer: S) -> Result 7 | where 8 | S: serde::Serializer, 9 | { 10 | match self { 11 | Variant::Null => serializer.serialize_none(), 12 | Variant::Number(v) => v.serialize(serializer), 13 | Variant::String(v) => serializer.serialize_str(v), 14 | Variant::Bool(v) => serializer.serialize_bool(*v), 15 | Variant::Bytes(v) => { 16 | let mut seq = serializer.serialize_seq(Some(v.len()))?; 17 | for item in v { 18 | seq.serialize_element(item)?; 19 | } 20 | seq.end() 21 | } 22 | Variant::Regexp(v) => serializer.serialize_str(v.as_str()), 23 | Variant::Date(v) => { 24 | let ts = v.duration_since(UNIX_EPOCH).map_err(serde::ser::Error::custom)?; 25 | serializer.serialize_u64(ts.as_millis() as u64) 26 | } 27 | Variant::Array(v) => { 28 | let mut seq = serializer.serialize_seq(Some(v.len()))?; 29 | for item in v { 30 | seq.serialize_element(item)?; 31 | } 32 | seq.end() 33 | } 34 | Variant::Object(v) => { 35 | let mut map = serializer.serialize_map(Some(v.len()))?; 36 | for (k, v) in v { 37 | map.serialize_entry(k, v)?; 38 | } 39 | map.end() 40 | } 41 | } 42 | } 43 | } 44 | 45 | impl<'de> Deserialize<'de> for Variant { 46 | fn deserialize(deserializer: D) -> Result 47 | where 48 | D: Deserializer<'de>, 49 | { 50 | struct VariantVisitor; 51 | 52 | impl<'de> de::Visitor<'de> for VariantVisitor { 53 | type Value = Variant; 54 | 55 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 56 | write!(formatter, "a valid Variant value") 57 | } 58 | 59 | fn visit_unit(self) -> Result 60 | where 61 | E: de::Error, 62 | { 63 | Ok(Variant::Null) 64 | } 65 | 66 | fn visit_bool(self, value: bool) -> Result 67 | where 68 | E: de::Error, 69 | { 70 | Ok(Variant::Bool(value)) 71 | } 72 | 73 | fn visit_i64(self, value: i64) -> Result 74 | where 75 | E: de::Error, 76 | { 77 | Ok(Variant::Number(value.into())) 78 | } 79 | 80 | fn visit_u64(self, value: u64) -> Result 81 | where 82 | E: de::Error, 83 | { 84 | Ok(Variant::Number(value.into())) 85 | } 86 | 87 | fn visit_f64(self, value: f64) -> Result 88 | where 89 | E: de::Error, 90 | { 91 | Ok(Variant::Number(serde_json::Number::from_f64(value).unwrap())) 92 | } 93 | 94 | fn visit_str(self, value: &str) -> Result 95 | where 96 | E: de::Error, 97 | { 98 | Ok(Variant::String(value.to_owned())) 99 | } 100 | 101 | fn visit_bytes(self, value: &[u8]) -> Result 102 | where 103 | E: de::Error, 104 | { 105 | Ok(Variant::Bytes(value.to_vec())) 106 | } 107 | 108 | fn visit_seq(self, mut seq: A) -> Result 109 | where 110 | A: de::SeqAccess<'de>, 111 | { 112 | let mut vec = Vec::new(); 113 | while let Some(item) = seq.next_element()? { 114 | vec.push(item); 115 | } 116 | Ok(Variant::Array(vec)) 117 | } 118 | 119 | fn visit_map(self, mut map: A) -> Result 120 | where 121 | A: de::MapAccess<'de>, 122 | { 123 | let mut btreemap = VariantObjectMap::new(); 124 | while let Some((key, value)) = map.next_entry()? { 125 | btreemap.insert(key, value); 126 | } 127 | Ok(Variant::Object(btreemap)) 128 | } 129 | } 130 | 131 | deserializer.deserialize_any(VariantVisitor) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /crates/core/src/runtime/nodes/common_nodes/catch.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use serde::Deserialize; 4 | 5 | use crate::runtime::flow::Flow; 6 | use crate::runtime::nodes::*; 7 | use edgelink_macro::*; 8 | 9 | #[flow_node("catch")] 10 | #[derive(Debug)] 11 | pub struct CatchNode { 12 | base: FlowNode, 13 | pub scope: CatchNodeScope, 14 | pub uncaught: bool, 15 | } 16 | 17 | #[derive(Debug, Clone, Default, PartialEq, Eq)] 18 | pub enum CatchNodeScope { 19 | #[default] 20 | All, 21 | Group, 22 | Nodes(Vec), 23 | } 24 | 25 | impl CatchNodeScope { 26 | pub fn as_bool(&self) -> bool { 27 | !matches!(self, CatchNodeScope::All) 28 | } 29 | } 30 | 31 | #[derive(Debug, Default, Deserialize)] 32 | struct CatchNodeConfig { 33 | #[serde(default)] 34 | scope: CatchNodeScope, 35 | 36 | #[serde(default)] 37 | uncaught: bool, 38 | } 39 | 40 | impl CatchNode { 41 | fn build(_flow: &Flow, state: FlowNode, _config: &RedFlowNodeConfig) -> crate::Result> { 42 | let catch_config = CatchNodeConfig::deserialize(&_config.rest)?; 43 | let node = CatchNode { base: state, scope: catch_config.scope, uncaught: catch_config.uncaught }; 44 | Ok(Box::new(node)) 45 | } 46 | } 47 | 48 | #[async_trait] 49 | impl FlowNodeBehavior for CatchNode { 50 | fn get_node(&self) -> &FlowNode { 51 | &self.base 52 | } 53 | 54 | async fn run(self: Arc, stop_token: CancellationToken) { 55 | while !stop_token.is_cancelled() { 56 | let cancel = stop_token.child_token(); 57 | with_uow(self.as_ref(), cancel.child_token(), |node, msg| async move { 58 | node.fan_out_one(Envelope { port: 0, msg }, cancel.child_token()).await?; 59 | Ok(()) 60 | }) 61 | .await; 62 | } 63 | } 64 | } 65 | 66 | impl<'de> Deserialize<'de> for CatchNodeScope { 67 | fn deserialize(deserializer: D) -> std::result::Result 68 | where 69 | D: serde::Deserializer<'de>, 70 | { 71 | struct CatchNodeScopeVisitor; 72 | 73 | impl<'de> serde::de::Visitor<'de> for CatchNodeScopeVisitor { 74 | type Value = CatchNodeScope; 75 | 76 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 77 | formatter.write_str("a string, null, or an array of strings") 78 | } 79 | 80 | fn visit_unit(self) -> Result 81 | where 82 | E: serde::de::Error, 83 | { 84 | Ok(CatchNodeScope::All) 85 | } 86 | 87 | fn visit_str(self, value: &str) -> Result 88 | where 89 | E: serde::de::Error, 90 | { 91 | match value { 92 | "group" => Ok(CatchNodeScope::Group), 93 | _ => Err(serde::de::Error::invalid_value(serde::de::Unexpected::Str(value), &self)), 94 | } 95 | } 96 | 97 | fn visit_seq(self, seq: A) -> Result 98 | where 99 | A: serde::de::SeqAccess<'de>, 100 | { 101 | let vec: Vec = Deserialize::deserialize(serde::de::value::SeqAccessDeserializer::new(seq))?; 102 | Ok(CatchNodeScope::Nodes(vec)) 103 | } 104 | } 105 | 106 | deserializer.deserialize_any(CatchNodeScopeVisitor) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /crates/core/src/runtime/nodes/common_nodes/complete.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::runtime::flow::Flow; 4 | use crate::runtime::model::*; 5 | use crate::runtime::nodes::*; 6 | use edgelink_macro::*; 7 | 8 | #[derive(Debug)] 9 | #[flow_node("complete")] 10 | struct CompleteNode { 11 | base: FlowNode, 12 | } 13 | 14 | impl CompleteNode { 15 | fn build(_flow: &Flow, state: FlowNode, _config: &RedFlowNodeConfig) -> crate::Result> { 16 | let node = CompleteNode { base: state }; 17 | Ok(Box::new(node)) 18 | } 19 | } 20 | 21 | #[async_trait] 22 | impl FlowNodeBehavior for CompleteNode { 23 | fn get_node(&self) -> &FlowNode { 24 | &self.base 25 | } 26 | 27 | async fn run(self: Arc, stop_token: CancellationToken) { 28 | while !stop_token.is_cancelled() { 29 | // We are not using the Unit of Work stuff here! 30 | match self.recv_msg(stop_token.clone()).await { 31 | Ok(msg) => match self.fan_out_one(Envelope { port: 0, msg }, stop_token.clone()).await { 32 | Ok(_) => {} 33 | Err(err) => { 34 | log::error!( 35 | "Fatal error: failed to fan out message in CompleteNode(id='{}', name='{}'): {:?}", 36 | self.id(), 37 | self.name(), 38 | err 39 | ); 40 | } 41 | }, 42 | Err(ref err) => { 43 | log::error!("Error: {:#?}", err); 44 | break; 45 | } 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /crates/core/src/runtime/nodes/common_nodes/console_json.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use tokio::io::{self, AsyncWriteExt}; 3 | 4 | use crate::runtime::flow::Flow; 5 | use crate::runtime::nodes::*; 6 | use edgelink_macro::*; 7 | 8 | #[derive(Debug)] 9 | #[flow_node("console-json")] 10 | struct ConsoleJsonNode { 11 | base: FlowNode, 12 | } 13 | 14 | impl ConsoleJsonNode { 15 | fn build(_flow: &Flow, state: FlowNode, _config: &RedFlowNodeConfig) -> crate::Result> { 16 | let node = ConsoleJsonNode { base: state }; 17 | Ok(Box::new(node)) 18 | } 19 | } 20 | 21 | #[async_trait] 22 | impl FlowNodeBehavior for ConsoleJsonNode { 23 | fn get_node(&self) -> &FlowNode { 24 | &self.base 25 | } 26 | 27 | async fn run(self: Arc, stop_token: CancellationToken) { 28 | while !stop_token.is_cancelled() { 29 | let cancel = stop_token.child_token(); 30 | with_uow(self.as_ref(), cancel.child_token(), |_, msg| async move { 31 | let guard = msg.read().await; 32 | let msg_to_ser = guard.clone(); 33 | let json_value = serde_json::to_value(&msg_to_ser)?; 34 | let json_text = serde_json::to_string(&json_value)?; 35 | let mut stdout = io::stdout(); 36 | stdout.write_all(&[0x1e]).await?; // add `0x1E` character 37 | stdout.write_all(json_text.as_bytes()).await?; 38 | stdout.write_all(b"\n").await?; // add `\n` 39 | stdout.flush().await?; 40 | Ok(()) 41 | }) 42 | .await; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crates/core/src/runtime/nodes/common_nodes/debug.rs: -------------------------------------------------------------------------------- 1 | use serde; 2 | use serde::Deserialize; 3 | use std::sync::Arc; 4 | 5 | use crate::runtime::flow::Flow; 6 | use crate::runtime::model::json::RedFlowNodeConfig; 7 | use crate::runtime::nodes::*; 8 | use edgelink_macro::*; 9 | 10 | #[derive(Deserialize, Debug)] 11 | struct DebugNodeConfig { 12 | //#[serde(default)] 13 | //console: bool, 14 | //#[serde(default)] 15 | //target_type: String, 16 | #[serde(default)] 17 | complete: String, 18 | } 19 | 20 | #[derive(Debug)] 21 | #[flow_node("debug")] 22 | struct DebugNode { 23 | base: FlowNode, 24 | _config: DebugNodeConfig, 25 | } 26 | 27 | impl DebugNode { 28 | fn build(_flow: &Flow, state: FlowNode, config: &RedFlowNodeConfig) -> crate::Result> { 29 | let mut debug_config: DebugNodeConfig = DebugNodeConfig::deserialize(&config.rest)?; 30 | if debug_config.complete.is_empty() { 31 | debug_config.complete = "payload".to_string(); 32 | } 33 | 34 | let node = DebugNode { base: state, _config: debug_config }; 35 | Ok(Box::new(node)) 36 | } 37 | } 38 | 39 | #[async_trait] 40 | impl FlowNodeBehavior for DebugNode { 41 | fn get_node(&self) -> &FlowNode { 42 | &self.base 43 | } 44 | 45 | async fn run(self: Arc, stop_token: CancellationToken) { 46 | while !stop_token.is_cancelled() { 47 | if self.base.active { 48 | match self.recv_msg(stop_token.child_token()).await { 49 | Ok(msg) => { 50 | let msg = msg.read().await; 51 | log::info!("[debug:{}] Message Received: \n{:#?}", self.name(), &msg) 52 | } 53 | Err(ref err) => { 54 | log::error!("[debug:{}] Error: {:#?}", self.name(), err); 55 | break; 56 | } 57 | } 58 | } else { 59 | stop_token.cancelled().await; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /crates/core/src/runtime/nodes/common_nodes/junction.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::runtime::flow::Flow; 4 | use crate::runtime::nodes::*; 5 | use edgelink_macro::*; 6 | 7 | #[flow_node("junction")] 8 | struct JunctionNode { 9 | base: FlowNode, 10 | } 11 | 12 | impl JunctionNode { 13 | fn build(_flow: &Flow, state: FlowNode, _config: &RedFlowNodeConfig) -> crate::Result> { 14 | let node = JunctionNode { base: state }; 15 | Ok(Box::new(node)) 16 | } 17 | } 18 | 19 | #[async_trait] 20 | impl FlowNodeBehavior for JunctionNode { 21 | fn get_node(&self) -> &FlowNode { 22 | &self.base 23 | } 24 | 25 | async fn run(self: Arc, stop_token: CancellationToken) { 26 | while !stop_token.is_cancelled() { 27 | let cancel = stop_token.child_token(); 28 | with_uow(self.as_ref(), cancel.child_token(), |node, msg| async move { 29 | node.fan_out_one(Envelope { port: 0, msg }, cancel.child_token()).await?; 30 | Ok(()) 31 | }) 32 | .await; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /crates/core/src/runtime/nodes/common_nodes/link_in.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::runtime::flow::Flow; 4 | use crate::runtime::nodes::*; 5 | use edgelink_macro::*; 6 | 7 | #[derive(Debug)] 8 | #[flow_node("link in")] 9 | struct LinkInNode { 10 | base: FlowNode, 11 | } 12 | 13 | impl LinkInNode { 14 | fn build(_flow: &Flow, state: FlowNode, _config: &RedFlowNodeConfig) -> crate::Result> { 15 | let node = LinkInNode { base: state }; 16 | Ok(Box::new(node)) 17 | } 18 | } 19 | 20 | #[async_trait] 21 | impl FlowNodeBehavior for LinkInNode { 22 | fn get_node(&self) -> &FlowNode { 23 | &self.base 24 | } 25 | 26 | async fn run(self: Arc, stop_token: CancellationToken) { 27 | while !stop_token.is_cancelled() { 28 | let cancel = stop_token.clone(); 29 | with_uow(self.as_ref(), cancel.child_token(), |node, msg| async move { 30 | node.fan_out_one(Envelope { port: 0, msg }, cancel.clone()).await 31 | }) 32 | .await; 33 | } 34 | } 35 | } 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use super::*; 40 | use serde::Deserialize; 41 | use serde_json::json; 42 | 43 | #[tokio::test(flavor = "multi_thread", worker_threads = 4)] 44 | async fn test_it_should_be_linked_to_multiple_nodes() { 45 | let flows_json = json!([ 46 | {"id": "100", "type": "tab"}, 47 | {"id": "1", "z": "100", "type": "link out", 48 | "name": "link-out", "links": ["2", "3"]}, 49 | {"id": "2", "z": "100", "type": "link in", 50 | "name": "link-in0", "wires": [["4"]]}, 51 | {"id": "3", "z": "100", "type": "link in", 52 | "name": "link-in1", "wires": [["4"]]}, 53 | {"id": "4", "z": "100", "type": "test-once"} 54 | ]); 55 | let msgs_to_inject_json = json!([ 56 | ["1", {"payload": "hello"}], 57 | ]); 58 | 59 | let engine = crate::runtime::engine::build_test_engine(flows_json.clone()).unwrap(); 60 | let msgs_to_inject = Vec::<(ElementId, Msg)>::deserialize(msgs_to_inject_json.clone()).unwrap(); 61 | let msgs = 62 | engine.run_once_with_inject(2, std::time::Duration::from_secs_f64(0.4), msgs_to_inject).await.unwrap(); 63 | assert_eq!(msgs.len(), 2); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /crates/core/src/runtime/nodes/common_nodes/link_out.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use common_nodes::link_call::LinkCallNode; 4 | use serde::Deserialize; 5 | 6 | use crate::runtime::flow::Flow; 7 | use crate::runtime::nodes::*; 8 | use edgelink_macro::*; 9 | 10 | #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Deserialize)] 11 | enum LinkOutMode { 12 | #[default] 13 | #[serde(rename = "link")] 14 | Link = 0, 15 | 16 | #[serde(rename = "return")] 17 | Return = 1, 18 | } 19 | 20 | #[derive(Deserialize, Debug)] 21 | struct LinkOutNodeConfig { 22 | #[serde(default)] 23 | mode: LinkOutMode, 24 | 25 | #[serde(default, deserialize_with = "crate::runtime::model::json::deser::deser_red_id_vec")] 26 | links: Vec, 27 | } 28 | 29 | #[derive(Debug)] 30 | #[flow_node("link out")] 31 | struct LinkOutNode { 32 | base: FlowNode, 33 | mode: LinkOutMode, 34 | linked_nodes: Vec>, 35 | } 36 | 37 | impl LinkOutNode { 38 | fn build(flow: &Flow, state: FlowNode, _config: &RedFlowNodeConfig) -> crate::Result> { 39 | let link_out_config = LinkOutNodeConfig::deserialize(&_config.rest)?; 40 | let engine = flow.engine().expect("The engine must be created!"); 41 | 42 | let mut linked_nodes = Vec::new(); 43 | if link_out_config.mode == LinkOutMode::Link { 44 | for link_in_id in link_out_config.links.iter() { 45 | if let Some(link_in) = flow.get_node_by_id(link_in_id) { 46 | linked_nodes.push(Arc::downgrade(&link_in)); 47 | } else if let Some(link_in) = engine.find_flow_node_by_id(link_in_id) { 48 | linked_nodes.push(Arc::downgrade(&link_in)); 49 | } else { 50 | log::error!("LinkOutNode: Cannot found the required `link in` node(id={})!", link_in_id); 51 | return Err( 52 | EdgelinkError::BadFlowsJson("Cannot found the required `link in` node".to_string()).into() 53 | ); 54 | } 55 | } 56 | } 57 | 58 | let node = LinkOutNode { base: state, mode: link_out_config.mode, linked_nodes }; 59 | Ok(Box::new(node)) 60 | } 61 | 62 | async fn uow(&self, msg: MsgHandle, cancel: CancellationToken) -> crate::Result<()> { 63 | match self.mode { 64 | LinkOutMode::Link => { 65 | let mut is_msg_sent = false; 66 | for link_node in self.linked_nodes.iter() { 67 | if let Some(link_node) = link_node.upgrade() { 68 | let cloned_msg = if is_msg_sent { msg.deep_clone(true).await } else { msg.clone() }; 69 | is_msg_sent = true; 70 | link_node.inject_msg(cloned_msg, cancel.clone()).await?; 71 | } else { 72 | let err_msg = 73 | format!("The required `link in` was unavailable in `link out` node(id={})!", self.id()); 74 | return Err(EdgelinkError::InvalidOperation(err_msg).into()); 75 | } 76 | } 77 | } 78 | LinkOutMode::Return => { 79 | let flow = self.get_node().flow.upgrade().expect("The flow cannot be released!"); 80 | let engine = flow.engine().expect("The engine cannot be released"); 81 | let stack_top = { 82 | let mut msg_guard = msg.write().await; 83 | msg_guard.pop_link_source() 84 | }; 85 | if let Some(ref source_link) = stack_top { 86 | if let Some(target_node) = engine.find_flow_node_by_id(&source_link.link_call_node_id) { 87 | if let Some(link_call_node) = target_node.as_any().downcast_ref::() { 88 | link_call_node 89 | .return_msg(msg.clone(), source_link.id, self.id(), flow.id(), cancel.clone()) 90 | .await?; 91 | } else { 92 | return Err(EdgelinkError::InvalidOperation(format!( 93 | "The node(id='{}') is not a `link call` node!", 94 | source_link.link_call_node_id 95 | )) 96 | .into()); 97 | } 98 | } else { 99 | return Err(EdgelinkError::InvalidOperation(format!( 100 | "Cannot found the `link call` node by id='{}'", 101 | source_link.link_call_node_id 102 | )) 103 | .into()); 104 | } 105 | } else { 106 | return Err(EdgelinkError::InvalidOperation(format!( 107 | "The `link call stack` is empty for msg: {:?}", 108 | msg 109 | )) 110 | .into()); 111 | } 112 | } 113 | } 114 | 115 | Ok(()) 116 | } 117 | } 118 | 119 | #[async_trait] 120 | impl FlowNodeBehavior for LinkOutNode { 121 | fn get_node(&self) -> &FlowNode { 122 | &self.base 123 | } 124 | 125 | async fn run(self: Arc, stop_token: CancellationToken) { 126 | while !stop_token.is_cancelled() { 127 | let cancel = stop_token.clone(); 128 | with_uow(self.as_ref(), stop_token.clone(), |node, msg| node.uow(msg, cancel.clone())).await; 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /crates/core/src/runtime/nodes/common_nodes/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod catch; 2 | mod complete; 3 | mod console_json; 4 | mod debug; 5 | mod inject; 6 | mod junction; 7 | pub(crate) mod link_call; 8 | mod link_in; 9 | mod link_out; 10 | mod status; 11 | mod subflow; 12 | mod unknown; 13 | 14 | #[cfg(any(test, feature = "pymod"))] 15 | mod test_once; 16 | -------------------------------------------------------------------------------- /crates/core/src/runtime/nodes/common_nodes/status.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::runtime::flow::Flow; 4 | use crate::runtime::nodes::*; 5 | use edgelink_macro::*; 6 | 7 | #[flow_node("status")] 8 | struct StatusNode { 9 | base: FlowNode, 10 | } 11 | 12 | impl StatusNode { 13 | fn build(_flow: &Flow, state: FlowNode, _config: &RedFlowNodeConfig) -> crate::Result> { 14 | let node = StatusNode { base: state }; 15 | Ok(Box::new(node)) 16 | } 17 | } 18 | 19 | #[async_trait] 20 | impl FlowNodeBehavior for StatusNode { 21 | fn get_node(&self) -> &FlowNode { 22 | &self.base 23 | } 24 | 25 | async fn run(self: Arc, stop_token: CancellationToken) { 26 | while !stop_token.is_cancelled() { 27 | let cancel = stop_token.child_token(); 28 | with_uow(self.as_ref(), cancel.child_token(), |node, msg| async move { 29 | node.fan_out_one(Envelope { port: 0, msg }, cancel.child_token()).await?; 30 | Ok(()) 31 | }) 32 | .await; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /crates/core/src/runtime/nodes/common_nodes/subflow.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::runtime::flow::Flow; 4 | use crate::runtime::model::json::helpers; 5 | use crate::runtime::nodes::*; 6 | use edgelink_macro::*; 7 | 8 | #[derive(Debug)] 9 | #[flow_node("subflow")] 10 | struct SubflowNode { 11 | base: FlowNode, 12 | subflow_id: ElementId, 13 | } 14 | 15 | impl SubflowNode { 16 | fn build(_flow: &Flow, state: FlowNode, config: &RedFlowNodeConfig) -> crate::Result> { 17 | let subflow_id = config 18 | .type_name 19 | .split_once(':') 20 | .and_then(|p| helpers::parse_red_id_str(p.1)) 21 | .ok_or(EdgelinkError::BadArgument("config")) 22 | .with_context(|| format!("Bad subflow instance type: `{}`", config.type_name))?; 23 | 24 | //let subflow = flow.engine.upgrade().unwrap().flows 25 | let node = SubflowNode { base: state, subflow_id }; 26 | Ok(Box::new(node)) 27 | } 28 | } 29 | 30 | #[async_trait] 31 | impl FlowNodeBehavior for SubflowNode { 32 | fn get_node(&self) -> &FlowNode { 33 | &self.base 34 | } 35 | 36 | async fn run(self: Arc, stop_token: CancellationToken) { 37 | while !stop_token.is_cancelled() { 38 | let cancel = stop_token.clone(); 39 | with_uow(self.as_ref(), stop_token.clone(), |node, msg| async move { 40 | if let Some(engine) = node.get_node().flow.upgrade().and_then(|f| f.engine()) { 41 | engine.inject_msg_to_flow(node.subflow_id, msg, cancel.clone()).await?; 42 | } 43 | 44 | Ok(()) 45 | }) 46 | .await; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /crates/core/src/runtime/nodes/common_nodes/test_once.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::runtime::flow::Flow; 4 | use crate::runtime::nodes::*; 5 | use edgelink_macro::*; 6 | 7 | #[flow_node("test-once")] 8 | struct TestOnceNode { 9 | base: FlowNode, 10 | } 11 | 12 | impl TestOnceNode { 13 | fn build(_flow: &Flow, state: FlowNode, _config: &RedFlowNodeConfig) -> crate::Result> { 14 | let node = TestOnceNode { base: state }; 15 | Ok(Box::new(node)) 16 | } 17 | } 18 | 19 | #[async_trait] 20 | impl FlowNodeBehavior for TestOnceNode { 21 | fn get_node(&self) -> &FlowNode { 22 | &self.base 23 | } 24 | 25 | async fn run(self: Arc, stop_token: CancellationToken) { 26 | while !stop_token.is_cancelled() { 27 | let engine = self.engine().expect("The engine cannot be released"); 28 | 29 | match self.recv_msg(stop_token.clone()).await { 30 | Ok(msg) => engine.recv_final_msg(msg).expect("Shoud send final msg to the engine"), 31 | Err(e) => { 32 | match e.downcast_ref::() { 33 | Some(EdgelinkError::TaskCancelled) => (), 34 | None | Some(_) => eprintln!("Failed to recv_msg(): {:?}", e), 35 | } 36 | break; 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /crates/core/src/runtime/nodes/common_nodes/unknown.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::runtime::flow::Flow; 4 | use crate::runtime::nodes::*; 5 | use edgelink_macro::*; 6 | use runtime::engine::Engine; 7 | 8 | const UNKNOWN_GLOBAL_NODE_TYPE: &str = "unknown.global"; 9 | 10 | #[derive(Debug)] 11 | #[global_node("unknown.global")] 12 | struct UnknownGlobalNode { 13 | base: GlobalNode, 14 | } 15 | 16 | impl UnknownGlobalNode { 17 | fn build(engine: &Engine, config: &RedGlobalNodeConfig) -> crate::Result> { 18 | let context = engine.get_context_manager().new_context(&engine.context(), config.id.to_string()); 19 | let node = Self { 20 | base: GlobalNode { 21 | id: config.id, 22 | name: config.name.clone(), 23 | type_str: UNKNOWN_GLOBAL_NODE_TYPE, 24 | ordering: config.ordering, 25 | disabled: config.disabled, 26 | context, 27 | }, 28 | }; 29 | Ok(Box::new(node)) 30 | } 31 | } 32 | 33 | impl GlobalNodeBehavior for UnknownGlobalNode { 34 | fn get_node(&self) -> &GlobalNode { 35 | &self.base 36 | } 37 | } 38 | 39 | #[flow_node("unknown.flow")] 40 | struct UnknownFlowNode { 41 | base: FlowNode, 42 | } 43 | 44 | impl UnknownFlowNode { 45 | fn build(_flow: &Flow, base: FlowNode, _config: &RedFlowNodeConfig) -> crate::Result> { 46 | let node = UnknownFlowNode { base }; 47 | Ok(Box::new(node)) 48 | } 49 | } 50 | 51 | #[async_trait] 52 | impl FlowNodeBehavior for UnknownFlowNode { 53 | fn get_node(&self) -> &FlowNode { 54 | &self.base 55 | } 56 | 57 | async fn run(self: Arc, stop_token: CancellationToken) { 58 | while !stop_token.is_cancelled() { 59 | stop_token.cancelled().await; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crates/core/src/runtime/nodes/function_nodes/function/context_class.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use rquickjs::{class::Trace, Ctx, Function, IntoJs, Value}; 4 | use rquickjs::{prelude::*, Exception}; 5 | 6 | use crate::runtime::context::Context as RedContext; 7 | use crate::utils::async_util::SyncWaitableFuture; 8 | 9 | use super::{UndefinableVariant, Variant}; 10 | 11 | #[derive(Clone, Trace)] 12 | #[rquickjs::class(frozen)] 13 | pub(super) struct ContextClass { 14 | #[qjs(skip_trace)] 15 | pub red_ctx: Arc, 16 | } 17 | 18 | #[allow(non_snake_case)] 19 | #[rquickjs::methods] 20 | impl ContextClass { 21 | #[qjs(skip)] 22 | pub fn new(red_ctx: Arc) -> Self { 23 | ContextClass { red_ctx } 24 | } 25 | 26 | #[qjs(rename = "get")] 27 | pub fn get<'js>( 28 | self, 29 | keys: Value<'js>, 30 | store: Opt>, 31 | cb: Opt>, 32 | ctx: Ctx<'js>, 33 | ) -> rquickjs::Result> { 34 | let keys: String = keys.get()?; 35 | 36 | if let Some(cb) = cb.0 { 37 | let async_ctx = ctx.clone(); 38 | // User provides the callback, we do it in async 39 | ctx.spawn(async move { 40 | let store = store.0.and_then(|x| x.get::().ok()); 41 | match self.red_ctx.get_one(store.as_deref(), keys.as_ref(), &[]).await { 42 | Some(ctx_value) => { 43 | let args = (Value::new_undefined(async_ctx.clone()), ctx_value.into_js(&async_ctx)); 44 | cb.call::<_, ()>(args).unwrap(); 45 | } 46 | None => { 47 | let args = (Value::new_undefined(async_ctx.clone()), Value::new_undefined(async_ctx.clone())); 48 | cb.call::<_, ()>(args).unwrap(); 49 | } 50 | } 51 | }); 52 | Ok(Value::new_undefined(ctx.clone())) 53 | } else { 54 | // No callback, we do it in sync 55 | let store = store.0.and_then(|x| x.get::().ok()); 56 | let ctx_value = async move { self.red_ctx.get_one(store.as_deref(), keys.as_ref(), &[]).await }.wait(); 57 | UndefinableVariant(ctx_value).into_js(&ctx) 58 | } 59 | } 60 | 61 | #[qjs(rename = "set")] 62 | pub fn set<'js>( 63 | self, 64 | keys: Value<'js>, 65 | values: Value<'js>, 66 | store: Opt>, 67 | cb: Opt>, 68 | ctx: Ctx<'js>, 69 | ) -> rquickjs::Result<()> { 70 | let keys: String = keys.get()?; 71 | let values: Variant = values.get()?; 72 | 73 | if let Some(cb) = cb.0 { 74 | let async_ctx = ctx.clone(); 75 | // User provides the callback, we do it in async 76 | ctx.spawn(async move { 77 | let store = store.0.and_then(|x| x.get::().ok()); 78 | match self.red_ctx.set_one(store.as_deref(), keys.as_ref(), Some(values), &[]).await { 79 | Ok(()) => { 80 | let args = (Value::new_undefined(async_ctx.clone()),); 81 | cb.call::<_, ()>(args).unwrap(); 82 | } 83 | Err(_) => { 84 | let args = 85 | (Exception::from_message(async_ctx.clone(), "Failed to parse key").into_js(&async_ctx),); 86 | cb.call::<_, ()>(args).unwrap(); 87 | } 88 | } 89 | }); 90 | } else { 91 | // No callback, we do it in sync 92 | let store = store.0.and_then(|x| x.get::().ok()); 93 | async move { self.red_ctx.set_one(store.as_deref(), keys.as_ref(), Some(values), &[]).await } 94 | .wait() 95 | .map_err(|e| ctx.throw(format!("{}", e).into_js(&ctx).unwrap()))?; 96 | } 97 | Ok(()) 98 | } 99 | 100 | #[qjs(rename = "keys")] 101 | pub fn keys<'js>( 102 | self, 103 | store: Opt>, 104 | cb: Opt>, 105 | ctx: Ctx<'js>, 106 | ) -> rquickjs::Result> { 107 | let async_ctx = ctx.clone(); 108 | if let Some(cb) = cb.0 { 109 | // User provides the callback, we do it in async 110 | ctx.spawn(async move { 111 | let store = store.0.and_then(|x| x.get::().ok()); 112 | match self.red_ctx.keys(store.as_deref()).await { 113 | Some(ctx_keys) => { 114 | let args = (Value::new_undefined(async_ctx.clone()), ctx_keys.into_js(&async_ctx)); 115 | cb.call::<_, ()>(args).unwrap(); 116 | } 117 | None => { 118 | let args = (Value::new_undefined(async_ctx.clone()), Value::new_undefined(async_ctx.clone())); 119 | cb.call::<_, ()>(args).unwrap(); 120 | } 121 | } 122 | }); 123 | Ok(Value::new_undefined(ctx.clone())) 124 | } else { 125 | // No callback, we do it in sync 126 | let store = store.0.and_then(|x| x.get::().ok()); 127 | match async move { self.red_ctx.keys(store.as_deref()).await }.wait() { 128 | Some(ctx_keys) => ctx_keys.into_js(&ctx), 129 | None => Ok(Value::new_undefined(ctx.clone())), 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /crates/core/src/runtime/nodes/function_nodes/function/edgelink_class.rs: -------------------------------------------------------------------------------- 1 | use rquickjs::{class::Trace, Ctx, Result, Value}; 2 | 3 | use crate::runtime::js::util; 4 | 5 | #[derive(Clone, Trace, Default)] 6 | #[rquickjs::class(frozen)] 7 | pub(super) struct EdgelinkClass {} 8 | 9 | #[allow(non_snake_case)] 10 | #[rquickjs::methods] 11 | impl<'js> EdgelinkClass { 12 | /// Deep clone a JS object 13 | #[qjs(rename = "deepClone")] 14 | fn deep_clone(&self, obj: Value<'js>, ctx: Ctx<'js>) -> Result> { 15 | util::deep_clone(ctx, obj) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /crates/core/src/runtime/nodes/function_nodes/function/env_class.rs: -------------------------------------------------------------------------------- 1 | use rquickjs::{class::Trace, Ctx, IntoJs, Result, Value}; 2 | 3 | use crate::runtime::env::*; 4 | 5 | #[derive(Clone, Trace)] 6 | #[rquickjs::class(frozen)] 7 | pub(super) struct EnvClass { 8 | #[qjs(skip_trace)] 9 | pub envs: Envs, 10 | } 11 | 12 | #[allow(non_snake_case)] 13 | #[rquickjs::methods] 14 | impl<'js> EnvClass { 15 | #[qjs(skip)] 16 | pub fn new(envs: &Envs) -> Self { 17 | EnvClass { envs: envs.clone() } 18 | } 19 | 20 | #[qjs()] 21 | fn get(&self, key: Value<'js>, ctx: Ctx<'js>) -> Result> { 22 | let key: String = key.get()?; 23 | let res: Value<'js> = match self.envs.evalute_env(key.as_ref()) { 24 | Some(var) => var.into_js(&ctx)?, 25 | _ => Value::new_undefined(ctx), 26 | }; 27 | Ok(res) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /crates/core/src/runtime/nodes/function_nodes/function/function.prelude.js: -------------------------------------------------------------------------------- 1 | // Prelude script for every `function` node 2 | const RED = (function () { 3 | return { 4 | util: { 5 | 6 | __cloneDeep: function (value, map = new WeakMap()) { 7 | if (value === null || typeof value !== 'object') { 8 | return value; 9 | } 10 | 11 | if (map.has(value)) { 12 | return map.get(value); 13 | } 14 | 15 | if (value instanceof Date) { 16 | return new Date(value.getTime()); 17 | } 18 | 19 | if (value instanceof RegExp) { 20 | return new RegExp(value); 21 | } 22 | 23 | if (Array.isArray(value)) { 24 | const clonedArray = value.map(item => this.__cloneDeep(item, map)); 25 | map.set(value, clonedArray); 26 | return clonedArray; 27 | } 28 | 29 | const clonedObj = {}; 30 | map.set(value, clonedObj); 31 | 32 | for (const key in value) { 33 | if (value.hasOwnProperty(key)) { 34 | clonedObj[key] = this.__cloneDeep(value[key], map); 35 | } 36 | } 37 | 38 | return clonedObj; 39 | }, 40 | 41 | cloneMessage: function (msg) { 42 | // FROM node-red 43 | if (typeof msg !== "undefined" && msg !== null) { 44 | // Temporary fix for #97 45 | // TODO: remove this http-node-specific fix somehow 46 | var req = msg.req; 47 | var res = msg.res; 48 | delete msg.req; 49 | delete msg.res; 50 | var m = this.__cloneDeep(msg); 51 | if (req) { 52 | m.req = req; 53 | msg.req = req; 54 | } 55 | if (res) { 56 | m.res = res; 57 | msg.res = res; 58 | } 59 | return m; 60 | } 61 | return msg; 62 | 63 | } 64 | } 65 | }; 66 | })(); 67 | -------------------------------------------------------------------------------- /crates/core/src/runtime/nodes/function_nodes/mod.rs: -------------------------------------------------------------------------------- 1 | mod change; 2 | mod range; 3 | mod rbe; 4 | 5 | #[cfg(feature = "js")] 6 | mod function; 7 | -------------------------------------------------------------------------------- /crates/core/src/runtime/nodes/function_nodes/range.rs: -------------------------------------------------------------------------------- 1 | use core::f64; 2 | use std::str::FromStr; 3 | use std::sync::Arc; 4 | 5 | use crate::runtime::flow::Flow; 6 | use crate::runtime::model::*; 7 | use crate::runtime::nodes::*; 8 | use edgelink_macro::*; 9 | use serde::Deserialize; 10 | 11 | #[derive(Debug, Deserialize, Default)] 12 | enum RangeAction { 13 | #[default] 14 | #[serde(rename = "scale")] 15 | Scale, 16 | 17 | #[serde(rename = "drop")] 18 | Drop, 19 | 20 | #[serde(rename = "clamp")] 21 | Clamp, 22 | 23 | #[serde(rename = "roll")] 24 | Roll, 25 | } 26 | 27 | #[derive(Deserialize, Debug)] 28 | struct RangeNodeConfig { 29 | action: RangeAction, 30 | 31 | #[serde(default)] 32 | round: bool, 33 | 34 | #[serde(deserialize_with = "json::deser::deser_f64_or_string_nan")] 35 | minin: f64, 36 | 37 | #[serde(deserialize_with = "json::deser::deser_f64_or_string_nan")] 38 | maxin: f64, 39 | 40 | #[serde(deserialize_with = "json::deser::deser_f64_or_string_nan")] 41 | minout: f64, 42 | 43 | #[serde(deserialize_with = "json::deser::deser_f64_or_string_nan")] 44 | maxout: f64, 45 | 46 | #[serde(default = "default_config_property")] 47 | property: String, 48 | } 49 | 50 | fn default_config_property() -> String { 51 | "payload".to_string() 52 | } 53 | 54 | #[derive(Debug)] 55 | #[flow_node("range")] 56 | struct RangeNode { 57 | base: FlowNode, 58 | config: RangeNodeConfig, 59 | } 60 | 61 | impl RangeNode { 62 | fn build( 63 | _flow: &Flow, 64 | base_node: FlowNode, 65 | config: &RedFlowNodeConfig, 66 | ) -> crate::Result> { 67 | let range_config = RangeNodeConfig::deserialize(&config.rest)?; 68 | let node = RangeNode { base: base_node, config: range_config }; 69 | Ok(Box::new(node)) 70 | } 71 | 72 | fn do_range(&self, msg: &mut Msg) -> crate::Result<()> { 73 | if let Some(value) = msg.get_nav_stripped_mut(&self.config.property) { 74 | let mut n: f64 = match value { 75 | Variant::Number(num_value) => num_value 76 | .as_f64() 77 | .ok_or(EdgelinkError::OutOfRange) 78 | .with_context(|| format!("Cannot convert the number `{}` to float", num_value))?, 79 | Variant::String(s) => s.parse::()?, 80 | _ => f64::NAN, 81 | }; 82 | 83 | if !n.is_nan() { 84 | match self.config.action { 85 | RangeAction::Drop => { 86 | if n < self.config.minin || n > self.config.maxin { 87 | return Err(EdgelinkError::OutOfRange.into()); 88 | } 89 | } 90 | 91 | RangeAction::Clamp => n = n.clamp(self.config.minin, self.config.maxin), 92 | 93 | RangeAction::Roll => { 94 | let divisor = self.config.maxin - self.config.minin; 95 | n = ((n - self.config.minin) % divisor + divisor) % divisor + self.config.minin; 96 | } 97 | 98 | _ => {} 99 | }; 100 | 101 | let mut new_value = ((n - self.config.minin) / (self.config.maxin - self.config.minin) 102 | * (self.config.maxout - self.config.minout)) 103 | + self.config.minout; 104 | if self.config.round { 105 | new_value = new_value.round(); 106 | } 107 | 108 | *value = Variant::Number(serde_json::Number::from_f64(new_value).unwrap()); 109 | Ok(()) 110 | } else { 111 | Err(EdgelinkError::OutOfRange).with_context(|| format!("The value is not a numner: {:?}", value)) 112 | } 113 | } else { 114 | Ok(()) 115 | } 116 | } 117 | } 118 | 119 | impl FromStr for RangeAction { 120 | type Err = (); 121 | 122 | fn from_str(input: &str) -> Result { 123 | match input.to_lowercase().as_str() { 124 | "scale" => Ok(RangeAction::Scale), 125 | "drop" => Ok(RangeAction::Drop), 126 | "clamp" => Ok(RangeAction::Clamp), 127 | "roll" => Ok(RangeAction::Roll), 128 | _ => Err(()), 129 | } 130 | } 131 | } 132 | 133 | #[async_trait] 134 | impl FlowNodeBehavior for RangeNode { 135 | fn get_node(&self) -> &FlowNode { 136 | &self.base 137 | } 138 | 139 | async fn run(self: Arc, stop_token: CancellationToken) { 140 | while !stop_token.is_cancelled() { 141 | let cancel = stop_token.child_token(); 142 | with_uow(self.as_ref(), cancel.child_token(), |node, msg| async move { 143 | { 144 | let mut msg_guard = msg.write().await; 145 | node.do_range(&mut msg_guard)?; 146 | } 147 | node.fan_out_one(Envelope { port: 0, msg }, cancel.child_token()).await?; 148 | Ok(()) 149 | }) 150 | .await; 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /crates/core/src/runtime/nodes/network_nodes/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "nodes_udp")] 2 | mod udp_out; 3 | -------------------------------------------------------------------------------- /crates/core/src/runtime/nodes/network_nodes/udp_out.rs: -------------------------------------------------------------------------------- 1 | use std::net::{IpAddr, SocketAddr}; 2 | use std::sync::Arc; 3 | use tokio::net::UdpSocket; 4 | 5 | use base64::prelude::*; 6 | use serde::Deserialize; 7 | 8 | use crate::runtime::flow::Flow; 9 | use crate::runtime::nodes::*; 10 | use edgelink_macro::*; 11 | 12 | #[derive(Debug)] 13 | enum UdpMulticast { 14 | No, 15 | Board, 16 | Multi, 17 | } 18 | 19 | impl<'de> Deserialize<'de> for UdpMulticast { 20 | fn deserialize(deserializer: D) -> Result 21 | where 22 | D: serde::Deserializer<'de>, 23 | { 24 | let s = String::deserialize(deserializer)?; 25 | match s.as_str() { 26 | "false" => Ok(UdpMulticast::No), 27 | "board" => Ok(UdpMulticast::Board), 28 | "multi" => Ok(UdpMulticast::Multi), 29 | _ => Err(serde::de::Error::invalid_value( 30 | serde::de::Unexpected::Str(&s), 31 | &"expected 'false' or 'board' or 'multi'", 32 | )), 33 | } 34 | } 35 | } 36 | 37 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] 38 | enum UdpIpV { 39 | #[serde(rename = "udp4")] 40 | V4, 41 | 42 | #[serde(rename = "udp6")] 43 | V6, 44 | } 45 | 46 | #[derive(Debug)] 47 | #[flow_node("udp out")] 48 | struct UdpOutNode { 49 | base: FlowNode, 50 | config: UdpOutNodeConfig, 51 | } 52 | 53 | impl UdpOutNode { 54 | fn build(_flow: &Flow, state: FlowNode, config: &RedFlowNodeConfig) -> crate::Result> { 55 | let udp_config = UdpOutNodeConfig::deserialize(&config.rest)?; 56 | 57 | let node = UdpOutNode { base: state, config: udp_config }; 58 | Ok(Box::new(node)) 59 | } 60 | } 61 | 62 | #[derive(Deserialize, Debug)] 63 | struct UdpOutNodeConfig { 64 | /// Remote address 65 | #[serde(deserialize_with = "crate::runtime::model::json::deser::str_to_ipaddr")] 66 | addr: Option, 67 | 68 | /// Remote port 69 | #[serde(deserialize_with = "crate::runtime::model::json::deser::str_to_option_u16")] 70 | port: Option, 71 | 72 | /// Local address 73 | #[serde(deserialize_with = "crate::runtime::model::json::deser::str_to_ipaddr")] 74 | iface: Option, 75 | 76 | /// Local port 77 | #[serde(deserialize_with = "crate::runtime::model::json::deser::str_to_option_u16")] 78 | outport: Option, 79 | 80 | ipv: UdpIpV, 81 | 82 | #[serde(default)] 83 | base64: bool, 84 | //multicast: UdpMulticast, 85 | } 86 | 87 | impl UdpOutNode { 88 | async fn uow(&self, msg: MsgHandle, socket: &UdpSocket) -> crate::Result<()> { 89 | let msg_guard = msg.read().await; 90 | if let Some(payload) = msg_guard.get("payload") { 91 | let remote_addr = std::net::SocketAddr::new( 92 | self.config.addr.unwrap(), // TODO FIXME 93 | self.config.port.unwrap(), 94 | ); 95 | 96 | if let Some(bytes) = payload.as_bytes() { 97 | if self.config.base64 { 98 | let b64_str = BASE64_STANDARD.encode(bytes); 99 | let bytes = b64_str.as_bytes(); 100 | socket.send_to(bytes, remote_addr).await?; 101 | } else { 102 | socket.send_to(bytes, remote_addr).await?; 103 | } 104 | } 105 | if let Some(bytes) = payload.to_bytes() { 106 | socket.send_to(&bytes, remote_addr).await?; 107 | } else { 108 | log::warn!("Failed to convert payload into bytes"); 109 | } 110 | } 111 | 112 | Ok(()) 113 | } 114 | } 115 | 116 | #[async_trait] 117 | impl FlowNodeBehavior for UdpOutNode { 118 | fn get_node(&self) -> &FlowNode { 119 | &self.base 120 | } 121 | 122 | async fn run(self: Arc, stop_token: CancellationToken) { 123 | let local_addr: SocketAddr = match self.config.outport { 124 | Some(port) => SocketAddr::new(self.config.iface.unwrap(), port), 125 | _ => match self.config.ipv { 126 | UdpIpV::V4 => "0.0.0.0:0".parse().unwrap(), 127 | UdpIpV::V6 => "[::]:0".parse().unwrap(), 128 | }, 129 | }; 130 | 131 | match tokio::net::UdpSocket::bind(local_addr).await { 132 | Ok(socket) => { 133 | let socket = Arc::new(socket); 134 | while !stop_token.is_cancelled() { 135 | let cloned_socket = socket.clone(); 136 | 137 | let node = self.clone(); 138 | with_uow(node.as_ref(), stop_token.clone(), |node, msg| async move { 139 | node.uow(msg, &cloned_socket).await 140 | }) 141 | .await; 142 | } 143 | } 144 | 145 | Err(e) => { 146 | log::error!("Can not bind local address: {:?}", e); 147 | } 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /crates/core/src/runtime/registry.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::ops::Deref; 3 | use std::sync::Arc; 4 | 5 | use crate::runtime::nodes::*; 6 | 7 | inventory::collect!(MetaNode); 8 | 9 | pub trait Registry: 'static + Send + Sync { 10 | fn all(&self) -> &HashMap<&'static str, &'static MetaNode>; 11 | fn get(&self, type_name: &str) -> Option<&'static MetaNode>; 12 | } 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct RegistryHandle(Arc); 16 | 17 | impl Deref for RegistryHandle { 18 | type Target = Arc; 19 | fn deref(&self) -> &Self::Target { 20 | &self.0 21 | } 22 | } 23 | 24 | #[derive(Debug, Clone)] 25 | struct RegistryImpl { 26 | meta_nodes: Arc>, 27 | } 28 | 29 | #[derive(Debug)] 30 | pub struct RegistryBuilder { 31 | meta_nodes: HashMap<&'static str, &'static MetaNode>, 32 | } 33 | 34 | impl Default for RegistryBuilder { 35 | fn default() -> Self { 36 | Self::new().with_builtins() 37 | } 38 | } 39 | 40 | impl RegistryBuilder { 41 | pub fn new() -> Self { 42 | Self { meta_nodes: HashMap::new() } 43 | } 44 | 45 | pub fn register(mut self, meta_node: &'static MetaNode) -> Self { 46 | self.meta_nodes.insert(meta_node.type_, meta_node); 47 | self 48 | } 49 | 50 | pub fn with_builtins(mut self) -> Self { 51 | for meta in inventory::iter:: { 52 | log::debug!("[REGISTRY] Available built-in Node: '{}'", meta.type_); 53 | self.meta_nodes.insert(meta.type_, meta); 54 | } 55 | self 56 | } 57 | 58 | pub fn build(self) -> crate::Result { 59 | if self.meta_nodes.is_empty() { 60 | log::warn!("There are no meta node in the Registry!"); 61 | } 62 | 63 | let result = RegistryHandle(Arc::new(RegistryImpl { meta_nodes: Arc::new(self.meta_nodes) })); 64 | Ok(result) 65 | } 66 | } 67 | 68 | impl RegistryImpl {} 69 | 70 | impl Registry for RegistryImpl { 71 | fn all(&self) -> &HashMap<&'static str, &'static MetaNode> { 72 | &self.meta_nodes 73 | } 74 | 75 | fn get(&self, type_name: &str) -> Option<&'static MetaNode> { 76 | self.meta_nodes.get(type_name).copied() 77 | } 78 | } 79 | 80 | impl std::fmt::Debug for dyn Registry { 81 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 82 | f.debug_struct("Registry").field("meta_nodes", self.all()).finish() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /crates/core/src/runtime/subflow.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Weak}; 2 | 3 | use json::RedFlowConfig; 4 | use smallvec::SmallVec; 5 | use tokio::task::JoinSet; 6 | use tokio_util::sync::CancellationToken; 7 | 8 | use super::{ 9 | engine::Engine, 10 | flow::{Flow, FlowArgs}, 11 | nodes::FlowNodeBehavior, 12 | }; 13 | use crate::runtime::model::*; 14 | 15 | #[derive(Debug)] 16 | pub(crate) struct SubflowOutputPort { 17 | pub index: usize, 18 | pub instance_node: Option>, 19 | pub msg_tx: MsgSender, 20 | pub msg_rx: MsgReceiverHolder, 21 | } 22 | 23 | #[derive(Debug)] 24 | pub(crate) struct SubflowState { 25 | pub instance_node: Option>, 26 | pub in_nodes: std::sync::RwLock>>, 27 | pub tx_ports: std::sync::RwLock; 4]>>, 28 | pub tx_tasks: tokio::sync::Mutex>, 29 | } 30 | 31 | impl SubflowOutputPort { 32 | pub(crate) async fn tx_task(&self, stop_token: CancellationToken) { 33 | while !stop_token.is_cancelled() { 34 | match self.msg_rx.recv_msg(stop_token.clone()).await { 35 | Ok(msg) => { 36 | // Find out the subflow:xxx node 37 | if let Some(instance_node) = self.instance_node.clone().and_then(|x| x.upgrade()) { 38 | let envelope = Envelope { port: self.index, msg }; 39 | if let Err(e) = instance_node.fan_out_one(envelope, stop_token.clone()).await { 40 | log::warn!("Failed to fan-out message: {:?}", e); 41 | } 42 | } else { 43 | log::warn!("The sub-flow does not have a subflow node"); 44 | } 45 | } 46 | 47 | Err(e) => { 48 | log::error!("Failed to receive msg in subflow_tx_task: {:?}", e); 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | impl SubflowState { 56 | pub(crate) fn new(engine: &Engine, flow_config: &RedFlowConfig, args: &FlowArgs) -> crate::Result { 57 | let subflow_instance = flow_config.subflow_node_id.and_then(|x| engine.find_flow_node_by_id(&x)); 58 | 59 | // Add empty subflow forward ports 60 | let mut tx_ports = SmallVec::with_capacity(flow_config.out_ports.len()); 61 | for (index, _) in flow_config.out_ports.iter().enumerate() { 62 | let (msg_root_tx, msg_rx) = tokio::sync::mpsc::channel(args.node_msg_queue_capacity); 63 | 64 | tx_ports.insert( 65 | index, 66 | Arc::new(SubflowOutputPort { 67 | index, 68 | instance_node: subflow_instance.clone().map(|x| Arc::downgrade(&x)), 69 | msg_tx: msg_root_tx.clone(), 70 | msg_rx: MsgReceiverHolder::new(msg_rx), 71 | }), 72 | ); 73 | } 74 | 75 | let mut this = Self { 76 | instance_node: None, // 77 | in_nodes: std::sync::RwLock::new(Vec::new()), 78 | tx_tasks: tokio::sync::Mutex::new(JoinSet::new()), 79 | tx_ports: std::sync::RwLock::new(tx_ports), 80 | }; 81 | 82 | this.instance_node = subflow_instance; 83 | 84 | Ok(this) 85 | } 86 | 87 | pub(crate) fn populate_in_nodes(&self, flow: &Flow, flow_config: &RedFlowConfig) -> crate::Result<()> { 88 | // this is a subflow with in ports 89 | let mut in_nodes = self.in_nodes.write().expect("`in_nodes` write lock"); 90 | for wire_obj in flow_config.in_ports.iter().flat_map(|x| x.wires.iter()) { 91 | if let Some(node) = flow.get_node_by_id(&wire_obj.id) { 92 | in_nodes.push(node.clone()); 93 | } else { 94 | log::warn!("Can not found node(id='{}')", wire_obj.id); 95 | } 96 | } 97 | Ok(()) 98 | } 99 | 100 | pub(crate) async fn start_tx_tasks(&self, stop_token: CancellationToken) -> crate::Result<()> { 101 | let mut tasks = self.tx_tasks.lock().await; 102 | let tx_ports = self.tx_ports.read().expect("tx_ports read lock").clone(); 103 | for tx_port in tx_ports.iter() { 104 | let child_stop_token = stop_token.clone(); 105 | let port_cloned = tx_port.clone(); 106 | tasks.spawn(async move { 107 | port_cloned.tx_task(child_stop_token.clone()).await; 108 | }); 109 | } 110 | Ok(()) 111 | } 112 | 113 | /* 114 | async fn stop_tx_tasks(&mut self) -> crate::Result<()> { 115 | while self.tx_tasks.join_next().await.is_some() { 116 | // 117 | } 118 | Ok(()) 119 | } 120 | */ 121 | } 122 | -------------------------------------------------------------------------------- /crates/core/src/text/json.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | pub static EMPTY_ARRAY: Vec = Vec::new(); 4 | 5 | pub fn value_equals_str(jv: &Value, target_string: &str) -> bool { 6 | match jv.as_str() { 7 | Some(s) => s == target_string, 8 | _ => false, 9 | } 10 | } 11 | 12 | pub fn option_value_equals_str(jv: &Option<&Value>, target_string: &str) -> bool { 13 | match jv { 14 | Some(s) => value_equals_str(s, target_string), 15 | _ => false, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /crates/core/src/text/json_seq.rs: -------------------------------------------------------------------------------- 1 | /// JavaScript Object Notation (JSON) Text Sequences 2 | /// 3 | 4 | pub const RS_CHAR: u8 = 0x1e; 5 | pub const NL_CHAR: u8 = b'\n'; 6 | -------------------------------------------------------------------------------- /crates/core/src/text/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod json; 2 | pub mod json_seq; 3 | pub mod nom_parsers; 4 | pub mod parsing; 5 | pub mod regex; 6 | -------------------------------------------------------------------------------- /crates/core/src/text/nom_parsers.rs: -------------------------------------------------------------------------------- 1 | use nom::{ 2 | branch::alt, 3 | bytes::complete::{tag, take_while}, 4 | character::complete::{alpha1, alphanumeric1, space0}, 5 | combinator::recognize, 6 | error::{ParseError, VerboseError}, 7 | multi::many0, 8 | sequence::{delimited, pair}, 9 | IResult, Parser, 10 | }; 11 | 12 | pub fn spaces<'a, E: ParseError<&'a str>>(i: &'a str) -> IResult<&'a str, &'a str, E> { 13 | let chars = " \t\r\n"; 14 | 15 | // nom combinators like `take_while` return a function. That function is the 16 | // parser,to which we can pass the input 17 | take_while(move |c| chars.contains(c))(i) 18 | } 19 | 20 | pub fn js_identifier(input: &str) -> IResult<&str, &str, VerboseError<&str>> { 21 | recognize(pair(alt((tag("$"), alpha1, tag("_"))), many0(alt((alphanumeric1, tag("_")))))).parse(input) 22 | } 23 | 24 | pub fn identifier(input: &str) -> IResult<&str, &str, VerboseError<&str>> { 25 | recognize(pair(alt((alpha1, tag("_"))), many0(alt((alphanumeric1, tag("_")))))).parse(input) 26 | } 27 | 28 | pub fn identifier_token(input: &str) -> nom::IResult<&str, &str, VerboseError<&str>> { 29 | recognize(delimited(space0, identifier, space0)).parse(input) 30 | } 31 | 32 | #[cfg(test)] 33 | mod tests { 34 | use super::*; 35 | 36 | #[test] 37 | fn test_valid_identifiers() { 38 | assert_eq!(identifier("identifier"), Ok(("", "identifier"))); 39 | assert_eq!(identifier("_underscore"), Ok(("", "_underscore"))); 40 | assert_eq!(identifier("id123"), Ok(("", "id123"))); 41 | assert_eq!(identifier("longer_identifier_with_123"), Ok(("", "longer_identifier_with_123"))); 42 | assert_eq!(identifier("_"), Ok(("", "_"))); 43 | assert_eq!(identifier("my_id-"), Ok(("-", "my_id"))); 44 | } 45 | 46 | #[test] 47 | fn test_invalid_identifiers() { 48 | assert!(identifier("123start").is_err()); 49 | assert!(identifier_token("-leading").is_err()); 50 | assert!(identifier_token("-invalid-").is_err()); 51 | assert!(identifier("").is_err()); 52 | } 53 | 54 | #[test] 55 | fn test_identifier_edge_cases() { 56 | assert_eq!(identifier("_"), Ok(("", "_"))); 57 | assert_eq!(identifier("a"), Ok(("", "a"))); 58 | assert_eq!(identifier("a123"), Ok(("", "a123"))); 59 | assert_eq!(identifier("a_b_c_123"), Ok(("", "a_b_c_123"))); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /crates/core/src/text/parsing.rs: -------------------------------------------------------------------------------- 1 | /// Equals to Javascript's `parseFloat` 2 | pub fn parse_float_lossy(input: &str) -> Option { 3 | let input = input.trim(); 4 | let mut end_idx = 0; 5 | let mut has_decimal_point = false; 6 | let mut has_exponent = false; 7 | let mut has_digits = false; 8 | 9 | for (i, c) in input.char_indices() { 10 | if c.is_ascii_digit() { 11 | has_digits = true; 12 | end_idx = i + 1; 13 | } else if c == '.' && !has_decimal_point && !has_exponent { 14 | has_decimal_point = true; 15 | end_idx = i + 1; 16 | } else if (c == 'e' || c == 'E') && has_digits && !has_exponent { 17 | has_exponent = true; 18 | has_digits = false; 19 | end_idx = i + 1; 20 | } else if (c == '+' || c == '-') 21 | && (i == 0 || input.chars().nth(i - 1).map(|p| p == 'e' || p == 'E').unwrap_or(false)) 22 | { 23 | end_idx = i + 1; 24 | } else { 25 | break; 26 | } 27 | } 28 | 29 | if has_exponent && !has_digits { 30 | end_idx = 31 | input.char_indices().take_while(|&(_, c)| c != 'e' && c != 'E').map(|(i, _)| i).last().map_or(0, |i| i + 1); 32 | } 33 | 34 | if end_idx > 0 { 35 | input[..end_idx].parse::().ok() 36 | } else { 37 | None 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | 45 | #[test] 46 | fn test_valid_f64() { 47 | assert_eq!(parse_float_lossy::("123.456"), Some(123.456)); 48 | assert_eq!(parse_float_lossy::("-123.456"), Some(-123.456)); 49 | assert_eq!(parse_float_lossy::("1.23e+10"), Some(1.23e10)); 50 | assert_eq!(parse_float_lossy::("+12.34"), Some(12.34)); 51 | } 52 | 53 | #[test] 54 | fn test_valid_f32() { 55 | assert_eq!(parse_float_lossy::("123.456"), Some(123.456_f32)); 56 | assert_eq!(parse_float_lossy::("-123.456"), Some(-123.456_f32)); 57 | assert_eq!(parse_float_lossy::("1.23e+10"), Some(1.23e10_f32)); 58 | assert_eq!(parse_float_lossy::("+12.34"), Some(12.34_f32)); 59 | } 60 | 61 | #[test] 62 | fn test_with_whitespace() { 63 | assert_eq!(parse_float_lossy::(" 123.456 "), Some(123.456)); 64 | assert_eq!(parse_float_lossy::(" -12.34 "), Some(-12.34)); 65 | } 66 | 67 | #[test] 68 | fn test_invalid_numbers() { 69 | assert_eq!(parse_float_lossy::("abc"), None); 70 | assert_eq!(parse_float_lossy::("abc123"), None); 71 | assert_eq!(parse_float_lossy::("123abc"), Some(123.0)); 72 | assert_eq!(parse_float_lossy::(""), None); 73 | assert_eq!(parse_float_lossy::("5.0 deg"), Some(5.0)); 74 | assert_eq!(parse_float_lossy::("6 deg"), Some(6.0)); 75 | } 76 | 77 | #[test] 78 | fn test_edge_cases() { 79 | assert_eq!(parse_float_lossy::("."), None); 80 | assert_eq!(parse_float_lossy::("..123"), None); 81 | assert_eq!(parse_float_lossy::("123..456"), Some(123.0)); 82 | assert_eq!(parse_float_lossy::("1.23e"), Some(1.23)); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /crates/core/src/text/regex.rs: -------------------------------------------------------------------------------- 1 | pub mod serde_regex { 2 | use regex::Regex; 3 | use serde::{self, Deserialize, Deserializer, Serializer}; 4 | 5 | pub fn deserialize<'de, D>(deserializer: D) -> Result 6 | where 7 | D: Deserializer<'de>, 8 | { 9 | let s: String = String::deserialize(deserializer)?; 10 | Regex::new(&s).map_err(serde::de::Error::custom) 11 | } 12 | 13 | pub fn serialize(value: &Regex, serializer: S) -> Result 14 | where 15 | S: Serializer, 16 | { 17 | serializer.serialize_str(value.as_str()) 18 | } 19 | } 20 | 21 | pub mod serde_optional_regex { 22 | 23 | use regex::Regex; 24 | use serde::{self, Deserialize, Deserializer, Serializer}; 25 | 26 | pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> 27 | where 28 | D: Deserializer<'de>, 29 | { 30 | let opt: Option = Option::deserialize(deserializer)?; 31 | match opt { 32 | Some(s) => Regex::new(&s).map(Some).map_err(serde::de::Error::custom), 33 | None => Ok(None), 34 | } 35 | } 36 | 37 | pub fn serialize(value: &Option, serializer: S) -> Result 38 | where 39 | S: Serializer, 40 | { 41 | match value { 42 | Some(regex) => serializer.serialize_some(regex.as_str()), 43 | None => serializer.serialize_none(), 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crates/core/src/utils/async_util.rs: -------------------------------------------------------------------------------- 1 | use crate::EdgelinkError; 2 | use std::time::Duration; 3 | use tokio_util::sync::CancellationToken; 4 | 5 | pub async fn delay(dur: Duration, cancel: CancellationToken) -> crate::Result<()> { 6 | tokio::select! { 7 | _ = cancel.cancelled() => { 8 | // 取消 sleep_task 任务 9 | Err(EdgelinkError::TaskCancelled.into()) 10 | } 11 | _ = tokio::time::sleep(dur)=> { 12 | // Long work has completed 13 | Ok(()) 14 | } 15 | } 16 | } 17 | 18 | pub async fn delay_secs_f64(secs: f64, cancel: CancellationToken) -> crate::Result<()> { 19 | delay(Duration::from_secs_f64(secs), cancel).await 20 | } 21 | 22 | pub async fn delay_millis(millis: u64, cancel: CancellationToken) -> crate::Result<()> { 23 | delay(Duration::from_millis(millis), cancel).await 24 | } 25 | 26 | pub trait SyncWaitableFuture: std::future::Future { 27 | fn wait(self) -> Self::Output 28 | where 29 | Self: Sized + Send + 'static, 30 | Self::Output: Send + 'static, 31 | { 32 | let handle = tokio::runtime::Handle::current(); 33 | let task = handle.spawn(self); 34 | tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(task).unwrap()) 35 | } 36 | } 37 | 38 | impl SyncWaitableFuture for F where F: std::future::Future {} 39 | -------------------------------------------------------------------------------- /crates/core/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | use rand::Rng; 2 | use std::time::{SystemTime, UNIX_EPOCH}; 3 | 4 | pub mod async_util; 5 | 6 | #[allow(clippy::all)] 7 | pub mod graph; 8 | 9 | pub mod time; 10 | pub mod topo; 11 | 12 | pub fn generate_uid() -> u64 { 13 | let mut rng = rand::thread_rng(); 14 | let random_part: u64 = rng.gen(); 15 | let timestamp_part = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time error!!!").as_nanos() as u64; 16 | 17 | timestamp_part ^ random_part 18 | } 19 | 20 | pub fn generate_str_uid() -> String { 21 | format!("{:016x}", generate_uid()) 22 | } 23 | -------------------------------------------------------------------------------- /crates/core/src/utils/time.rs: -------------------------------------------------------------------------------- 1 | use chrono::prelude::Utc; 2 | use std::time::{SystemTime, UNIX_EPOCH}; 3 | 4 | pub fn unix_now() -> i64 { 5 | let now = SystemTime::now(); 6 | let epoch = UNIX_EPOCH; 7 | let duration = now.duration_since(epoch).unwrap(); 8 | duration.as_millis() as i64 9 | } 10 | 11 | pub fn iso_now() -> String { 12 | let now = Utc::now(); 13 | now.to_rfc3339() 14 | } 15 | 16 | pub fn millis_now() -> String { 17 | let now = Utc::now(); 18 | now.timestamp_millis().to_string() 19 | } 20 | -------------------------------------------------------------------------------- /crates/core/src/utils/topo.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::graph::Graph; 2 | 3 | #[derive(Clone)] 4 | pub struct TopologicalSorter { 5 | graph: Graph, 6 | } 7 | 8 | impl Default for TopologicalSorter 9 | where 10 | N: Clone + Eq + Ord, 11 | { 12 | fn default() -> Self { 13 | Self::new() 14 | } 15 | } 16 | 17 | impl TopologicalSorter { 18 | pub fn new() -> Self { 19 | TopologicalSorter { graph: Graph::new() } 20 | } 21 | 22 | pub fn add_vertex(&mut self, item: N) { 23 | self.graph.add(item) 24 | } 25 | 26 | pub fn add_dep(&mut self, from: N, to: N) { 27 | self.graph.add(from.clone()); 28 | let _ = self.graph.link(to, from.clone()); 29 | } 30 | 31 | pub fn add_deps(&mut self, from: N, tos: impl IntoIterator) { 32 | for to in tos { 33 | self.add_dep(from.clone(), to); 34 | } 35 | } 36 | 37 | pub fn topological_sort(&self) -> Vec { 38 | self.graph.sort() 39 | } 40 | 41 | pub fn dependency_sort(&self) -> Vec { 42 | let mut result = self.topological_sort(); 43 | result.reverse(); 44 | result 45 | } 46 | } 47 | 48 | #[cfg(test)] 49 | mod graph_tests { 50 | use super::*; 51 | 52 | #[test] 53 | fn test_simple_linear_dependency() { 54 | let mut graph = TopologicalSorter::new(); 55 | graph.add_dep("A", "B"); 56 | graph.add_dep("B", "C"); 57 | 58 | let sorted = graph.topological_sort(); 59 | assert_eq!(sorted, vec!["A", "B", "C"]); 60 | } 61 | 62 | #[test] 63 | fn test_multiple_sources() { 64 | let mut graph = TopologicalSorter::new(); 65 | graph.add_dep("A", "C"); 66 | graph.add_dep("B", "C"); 67 | 68 | let sorted = graph.topological_sort(); 69 | assert!(sorted == vec!["A", "B", "C"] || sorted == vec!["B", "A", "C"]); 70 | } 71 | 72 | #[test] 73 | fn test_complex_dependency() { 74 | let mut graph = TopologicalSorter::new(); 75 | graph.add_deps("A", ["B", "C"]); 76 | graph.add_dep("B", "D"); 77 | graph.add_dep("C", "D"); 78 | graph.add_dep("D", "E"); 79 | 80 | let sorted = graph.topological_sort(); 81 | assert!(sorted.contains(&"A")); 82 | assert!(sorted.contains(&"B")); 83 | assert!(sorted.contains(&"C")); 84 | assert!(sorted.contains(&"D")); 85 | assert!(sorted.contains(&"E")); 86 | 87 | let a_index = sorted.iter().position(|&x| x == "A").unwrap(); 88 | let b_index = sorted.iter().position(|&x| x == "B").unwrap(); 89 | let c_index = sorted.iter().position(|&x| x == "C").unwrap(); 90 | let d_index = sorted.iter().position(|&x| x == "D").unwrap(); 91 | let e_index = sorted.iter().position(|&x| x == "E").unwrap(); 92 | 93 | assert!(a_index < b_index); 94 | assert!(a_index < c_index); 95 | assert!(b_index < d_index); 96 | assert!(c_index < d_index); 97 | assert!(d_index < e_index); 98 | } 99 | 100 | #[test] 101 | fn test_multiple_layers() { 102 | let mut graph = TopologicalSorter::new(); 103 | graph.add_dep("A", "C"); 104 | graph.add_dep("B", "C"); 105 | graph.add_dep("C", "D"); 106 | graph.add_dep("D", "E"); 107 | graph.add_dep("E", "F"); 108 | 109 | let sorted = graph.topological_sort(); 110 | assert!(sorted == vec!["A", "B", "C", "D", "E", "F"] || sorted == vec!["B", "A", "C", "D", "E", "F"]); 111 | } 112 | 113 | #[test] 114 | fn test_cycle_processing() { 115 | let mut graph = TopologicalSorter::new(); 116 | graph.add_dep("A", "B"); 117 | graph.add_dep("B", "C"); 118 | graph.add_dep("C", "A"); 119 | 120 | let sorted = graph.dependency_sort(); 121 | assert!(sorted.len() == 3); 122 | assert!(sorted.contains(&"A")); 123 | assert!(sorted.contains(&"B")); 124 | assert!(sorted.contains(&"C")); 125 | } 126 | 127 | #[test] 128 | fn test_multiple_cycles() { 129 | let mut graph = TopologicalSorter::new(); 130 | graph.add_dep("A", "B"); 131 | graph.add_dep("B", "C"); 132 | graph.add_dep("C", "A"); 133 | graph.add_dep("D", "E"); 134 | graph.add_dep("E", "D"); 135 | 136 | let sorted = graph.dependency_sort(); 137 | assert!(sorted.len() == 5); 138 | assert!(sorted.contains(&"A")); 139 | assert!(sorted.contains(&"B")); 140 | assert!(sorted.contains(&"C")); 141 | assert!(sorted.contains(&"D")); 142 | assert!(sorted.contains(&"E")); 143 | } 144 | 145 | #[test] 146 | fn test_independent_nodes() { 147 | let mut graph = TopologicalSorter::new(); 148 | graph.add_dep("A", "B"); 149 | graph.add_dep("C", "D"); 150 | 151 | let sorted = graph.topological_sort(); 152 | assert!( 153 | sorted == vec!["A", "B", "C", "D"] 154 | || sorted == vec!["A", "C", "B", "D"] 155 | || sorted == vec!["C", "D", "A", "B"] 156 | ); 157 | } 158 | 159 | #[test] 160 | fn test_large_graph() { 161 | let mut graph = TopologicalSorter::new(); 162 | for i in 0..100 { 163 | for j in (i + 1)..100 { 164 | graph.add_dep(i.to_string(), j.to_string()); 165 | } 166 | } 167 | 168 | let sorted = graph.topological_sort(); 169 | for i in 0..100 { 170 | for j in (i + 1)..100 { 171 | assert!( 172 | sorted.iter().position(|x| x == &i.to_string()) < sorted.iter().position(|x| x == &j.to_string()) 173 | ); 174 | } 175 | } 176 | } 177 | 178 | #[test] 179 | fn test_single_node() { 180 | let mut graph = TopologicalSorter::new(); 181 | graph.add_dep("A", "A"); 182 | let sorted = graph.topological_sort(); 183 | 184 | assert_eq!(sorted, &["A"]) 185 | } 186 | 187 | #[test] 188 | fn test_no_dependencies() { 189 | let mut graph = TopologicalSorter::new(); 190 | graph.add_dep("A", "B"); 191 | graph.add_dep("C", "D"); 192 | graph.add_dep("E", "F"); 193 | 194 | let sorted = graph.topological_sort(); 195 | assert!(sorted.len() == 6); 196 | assert!(sorted.contains(&"A")); 197 | assert!(sorted.contains(&"B")); 198 | assert!(sorted.contains(&"C")); 199 | assert!(sorted.contains(&"D")); 200 | assert!(sorted.contains(&"E")); 201 | assert!(sorted.contains(&"F")); 202 | } 203 | 204 | #[test] 205 | fn test_dependency_sort() { 206 | let mut graph = TopologicalSorter::new(); 207 | graph.add_deps("A", ["B", "C"]); 208 | graph.add_dep("B", "D"); 209 | graph.add_dep("C", "D"); 210 | graph.add_dep("D", "E"); 211 | graph.add_vertex("F"); 212 | 213 | let sorted = graph.dependency_sort(); 214 | assert_eq!(sorted.len(), 6); 215 | assert!(sorted.contains(&"A")); 216 | assert!(sorted.contains(&"B")); 217 | assert!(sorted.contains(&"C")); 218 | assert!(sorted.contains(&"D")); 219 | assert!(sorted.contains(&"E")); 220 | assert!(sorted.contains(&"F")); 221 | 222 | let a_index = sorted.iter().position(|&x| x == "A").unwrap(); 223 | let b_index = sorted.iter().position(|&x| x == "B").unwrap(); 224 | let c_index = sorted.iter().position(|&x| x == "C").unwrap(); 225 | let d_index = sorted.iter().position(|&x| x == "D").unwrap(); 226 | let e_index = sorted.iter().position(|&x| x == "E").unwrap(); 227 | let f_index = sorted.iter().position(|&x| x == "F").unwrap(); 228 | 229 | assert!(f_index == 0 || f_index == 5); 230 | assert!(b_index < a_index); 231 | assert!(c_index < a_index); 232 | assert!(d_index < b_index); 233 | assert!(d_index < c_index); 234 | assert!(d_index < a_index); 235 | assert!(e_index < d_index); 236 | assert!(e_index < b_index); 237 | assert!(e_index < c_index); 238 | assert!(e_index < a_index); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /crates/macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "edgelink-macro" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | proc-macro = true 8 | 9 | [dependencies] 10 | syn = { version = "2", features = ["full"] } 11 | quote = "1" 12 | proc-macro2 = "1" 13 | -------------------------------------------------------------------------------- /crates/macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | use proc_macro::TokenStream; 3 | use quote::quote; 4 | use syn::{parse_macro_input, DeriveInput, Lit}; 5 | 6 | #[proc_macro_attribute] 7 | pub fn flow_node(attr: TokenStream, item: TokenStream) -> TokenStream { 8 | let input = parse_macro_input!(item as DeriveInput); 9 | 10 | let struct_name = &input.ident; 11 | // let meta_node_name_string = format!("__{}_meta_node", struct_name).to_uppercase(); 12 | // let meta_node_name = syn::Ident::new(&meta_node_name_string, struct_name.span()); 13 | 14 | // parse node_type 15 | let lit = parse_macro_input!(attr as Lit); 16 | let node_type = match lit { 17 | Lit::Str(lit_str) => lit_str.value(), 18 | _ => panic!("Expected a string literal for node_type"), 19 | }; 20 | 21 | let expanded = quote! { 22 | #input 23 | 24 | impl FlowsElement for #struct_name { 25 | fn id(&self) -> ElementId { 26 | self.get_node().id 27 | } 28 | 29 | fn name(&self) -> &str { 30 | &self.get_node().name 31 | } 32 | 33 | fn type_str(&self) -> &'static str { 34 | self.get_node().type_str 35 | } 36 | 37 | fn ordering(&self) -> usize { 38 | self.get_node().ordering 39 | } 40 | 41 | fn is_disabled(&self) -> bool { 42 | self.get_node().disabled 43 | } 44 | 45 | fn parent_element(&self) -> Option { 46 | self.get_node().flow.upgrade().map(|arc| arc.id()) 47 | } 48 | 49 | fn as_any(&self) -> &dyn ::std::any::Any { 50 | self 51 | } 52 | 53 | fn get_path(&self) -> String { 54 | format!("{}/{}", self.get_node().flow.upgrade().unwrap().get_path(), self.id()) 55 | } 56 | 57 | } 58 | 59 | impl ContextHolder for #struct_name { 60 | fn context(&self) -> Arc { 61 | self.get_node().context.clone() 62 | } 63 | } 64 | 65 | ::inventory::submit! { 66 | MetaNode { 67 | kind: NodeKind::Flow, 68 | type_: #node_type, 69 | factory: NodeFactory::Flow(#struct_name::build), 70 | } 71 | } 72 | }; // quote! 73 | 74 | TokenStream::from(expanded) 75 | } 76 | 77 | #[proc_macro_attribute] 78 | pub fn global_node(attr: TokenStream, item: TokenStream) -> TokenStream { 79 | let input = parse_macro_input!(item as DeriveInput); 80 | 81 | let struct_name = &input.ident; 82 | // let meta_node_name_string = format!("__{}_meta_node", struct_name).to_uppercase(); 83 | // let meta_node_name = syn::Ident::new(&meta_node_name_string, struct_name.span()); 84 | 85 | // parse node_type 86 | let lit = parse_macro_input!(attr as Lit); 87 | let node_type = match lit { 88 | Lit::Str(lit_str) => lit_str.value(), 89 | _ => panic!("Expected a string literal for node_type"), 90 | }; 91 | 92 | let expanded = quote! { 93 | #input 94 | 95 | impl FlowsElement for #struct_name { 96 | fn id(&self) -> ElementId { 97 | self.get_node().id 98 | } 99 | 100 | fn name(&self) -> &str { 101 | &self.get_node().name 102 | } 103 | 104 | fn type_str(&self) -> &'static str { 105 | self.get_node().type_str 106 | } 107 | 108 | fn ordering(&self) -> usize { 109 | self.get_node().ordering 110 | } 111 | 112 | fn is_disabled(&self) -> bool { 113 | self.get_node().disabled 114 | } 115 | 116 | fn parent_element(&self) -> Option { 117 | // TODO change it to engine 118 | log::warn!("Cannot get the parent element in global node"); 119 | None 120 | } 121 | 122 | fn as_any(&self) -> &dyn ::std::any::Any { 123 | self 124 | } 125 | 126 | fn get_path(&self) -> String { 127 | self.id().to_string() 128 | } 129 | } 130 | 131 | impl ContextHolder for #struct_name { 132 | fn context(&self) -> Arc { 133 | self.get_node().context.clone() 134 | } 135 | } 136 | 137 | ::inventory::submit! { 138 | MetaNode { 139 | kind: NodeKind::Global, 140 | type_: #node_type, 141 | factory: NodeFactory::Global(#struct_name::build), 142 | } 143 | } 144 | 145 | }; // quote! 146 | TokenStream::from(expanded) 147 | } 148 | -------------------------------------------------------------------------------- /crates/pymod/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "edgelink-pymod" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | name = "edgelink_pymod" 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | pyo3 = { version = "^0.20", features = ["extension-module"] } 12 | pyo3-asyncio = { version = "^0.20", features = ["tokio-runtime"] } 13 | tokio.workspace = true 14 | serde.workspace = true 15 | serde_json.workspace = true 16 | config.workspace = true 17 | log4rs.workspace = true 18 | log.workspace = true 19 | 20 | edgelink-core = { path = "../../crates/core", default-features = true, features = [ 21 | "pymod", "default" 22 | ] } 23 | # Node plug-ins: 24 | edgelink-nodes-dummy = { path = "../../node-plugins/edgelink-nodes-dummy" } 25 | -------------------------------------------------------------------------------- /crates/pymod/src/json.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | use pyo3::types::{PyBool, PyDict, PyFloat, PyInt, PyList, PyString, PyTuple}; 3 | use serde_json::{Map, Value}; 4 | 5 | pub fn py_object_to_json_value(obj: &PyAny) -> PyResult { 6 | if let Ok(list) = obj.downcast::() { 7 | let mut json_list = Vec::new(); 8 | for item in list.iter() { 9 | json_list.push(py_object_to_json_value(item)?); 10 | } 11 | Ok(Value::Array(json_list)) 12 | } else if let Ok(dict) = obj.downcast::() { 13 | let mut json_map = Map::new(); 14 | for (key, value) in dict.iter() { 15 | let key = key.extract::()?; 16 | let value = py_object_to_json_value(value)?; 17 | json_map.insert(key, value); 18 | } 19 | Ok(Value::Object(json_map)) 20 | } else if let Ok(tuple) = obj.downcast::() { 21 | let mut json_list = Vec::new(); 22 | for item in tuple.iter() { 23 | json_list.push(py_object_to_json_value(item)?); 24 | } 25 | Ok(Value::Array(json_list)) 26 | } else if let Ok(boolean) = obj.downcast::() { 27 | Ok(Value::Bool(boolean.extract::()?)) 28 | } else if let Ok(float) = obj.downcast::() { 29 | let num = float.extract::()?; 30 | Ok(serde_json::json!(num)) 31 | } else if let Ok(int) = obj.downcast::() { 32 | let num = int.extract::()?; 33 | Ok(serde_json::json!(num)) 34 | } else if let Ok(string) = obj.downcast::() { 35 | Ok(Value::String(string.extract::()?)) 36 | } else { 37 | Ok(Value::Null) 38 | } 39 | } 40 | 41 | pub fn json_value_to_py_object(py: Python, value: &Value) -> PyResult { 42 | match value { 43 | Value::Null => Ok(py.None()), 44 | Value::Bool(b) => Ok(b.into_py(py)), 45 | Value::Number(n) => { 46 | if let Some(int) = n.as_i64() { 47 | Ok(int.to_object(py)) 48 | } else if let Some(float) = n.as_f64() { 49 | Ok(PyFloat::new(py, float).into()) 50 | } else { 51 | Err(PyErr::new::("Invalid number type")) 52 | } 53 | } 54 | Value::String(s) => Ok(PyString::new(py, s).into()), 55 | Value::Array(arr) => { 56 | let list = PyList::empty(py); 57 | for item in arr { 58 | let py_item = json_value_to_py_object(py, item)?; 59 | list.append(py_item)?; 60 | } 61 | Ok(list.into()) 62 | } 63 | Value::Object(obj) => { 64 | let dict = PyDict::new(py); 65 | for (key, value) in obj { 66 | let py_key = PyString::new(py, key); 67 | let py_value = json_value_to_py_object(py, value)?; 68 | dict.set_item(py_key, py_value)?; 69 | } 70 | Ok(dict.into()) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /crates/pymod/src/lib.rs: -------------------------------------------------------------------------------- 1 | use edgelink_core::runtime::model::{ElementId, Msg}; 2 | use pyo3::{prelude::*, wrap_pyfunction}; 3 | use serde::Deserialize; 4 | 5 | use edgelink_core::runtime::engine::Engine; 6 | mod json; 7 | 8 | #[pymodule] 9 | fn edgelink_pymod(_py: Python, m: &PyModule) -> PyResult<()> { 10 | m.add_function(wrap_pyfunction!(rust_sleep, m)?)?; 11 | m.add_function(wrap_pyfunction!(run_flows_once, m)?)?; 12 | 13 | let stderr = log4rs::append::console::ConsoleAppender::builder() 14 | .target(log4rs::append::console::Target::Stderr) 15 | .encoder(Box::new(log4rs::encode::pattern::PatternEncoder::new("[{h({l})}]\t{m}{n}"))) 16 | .build(); 17 | 18 | let config = log4rs::Config::builder() 19 | .appender(log4rs::config::Appender::builder().build("stderr", Box::new(stderr))) 20 | .build(log4rs::config::Root::builder().appender("stderr").build(log::LevelFilter::Warn)) 21 | .unwrap(); // TODO FIXME 22 | 23 | let _ = log4rs::init_config(config).unwrap(); 24 | 25 | Ok(()) 26 | } 27 | 28 | #[pyfunction] 29 | fn rust_sleep(py: Python) -> PyResult<&PyAny> { 30 | pyo3_asyncio::tokio::future_into_py(py, async { 31 | eprintln!("Sleeping in Rust!"); 32 | tokio::time::sleep(std::time::Duration::from_secs(1)).await; 33 | Ok(()) 34 | }) 35 | } 36 | 37 | #[pyfunction] 38 | fn run_flows_once<'a>( 39 | py: Python<'a>, 40 | _expected_msgs: usize, 41 | _timeout: f64, 42 | py_json: &'a PyAny, 43 | msgs_json: &'a PyAny, 44 | app_cfg: &'a PyAny, 45 | ) -> PyResult<&'a PyAny> { 46 | let flows_json = json::py_object_to_json_value(py_json)?; 47 | let msgs_to_inject = { 48 | let json_msgs = json::py_object_to_json_value(msgs_json)?; 49 | Vec::<(ElementId, Msg)>::deserialize(json_msgs) 50 | .map_err(|e| PyErr::new::(format!("{}", e)))? 51 | }; 52 | let app_cfg = { 53 | if !app_cfg.is_none() { 54 | let app_cfg_json = json::py_object_to_json_value(app_cfg)?; 55 | let config = config::Config::try_from(&app_cfg_json) 56 | .map_err(|e| PyErr::new::(format!("{}", e)))?; 57 | Some(config) 58 | } else { 59 | None 60 | } 61 | }; 62 | 63 | let registry = edgelink_core::runtime::registry::RegistryBuilder::default() 64 | .build() 65 | .map_err(|e| PyErr::new::(format!("{}", e)))?; 66 | 67 | let engine = Engine::with_json(®istry, flows_json, app_cfg.as_ref()) 68 | .map_err(|e| PyErr::new::(format!("{}", e)))?; 69 | 70 | pyo3_asyncio::tokio::future_into_py(py, async move { 71 | let msgs = engine 72 | .run_once_with_inject(_expected_msgs, std::time::Duration::from_secs_f64(_timeout), msgs_to_inject) 73 | .await 74 | .map_err(|e| PyErr::new::(format!("{}", e)))?; 75 | 76 | let result_value = serde_json::to_value(&msgs) 77 | .map_err(|e| PyErr::new::(format!("{}", e)))?; 78 | 79 | Python::with_gil(|py| { 80 | let pyo = json::json_value_to_py_object(py, &result_value)?; 81 | Ok(pyo.to_object(py)) 82 | }) 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /edgelinkd.dev.toml: -------------------------------------------------------------------------------- 1 | [runtime.context.stores] 2 | memory0 = { provider = "memory" } 3 | memory1 = { provider = "memory" } 4 | memory2 = { provider = "memory" } 5 | -------------------------------------------------------------------------------- /edgelinkd.prod.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldrev/edgelink/21aaed943fdc9ba14b4919ce6296cf4eb1713690/edgelinkd.prod.toml -------------------------------------------------------------------------------- /edgelinkd.toml: -------------------------------------------------------------------------------- 1 | [runtime] 2 | 3 | [runtime.engine] 4 | 5 | [runtime.context] 6 | default = "memory" 7 | 8 | [runtime.context.stores] 9 | memory = { provider = "memory" } 10 | 11 | 12 | [runtime.flow] 13 | node_msg_queue_capacity = 16 14 | -------------------------------------------------------------------------------- /log.toml: -------------------------------------------------------------------------------- 1 | [root] 2 | level = "info" 3 | appenders = [ "stdout", "file" ] 4 | 5 | [appenders.stdout] 6 | kind = "console" 7 | encoder = { pattern = "[{h({l})}]\t{m}{n}"} 8 | #encoder = { pattern = "[{l}]\t{d} - {t} - {m}{n}" } 9 | 10 | [appenders.file] 11 | kind = "file" 12 | path = "log/edgelinkd.log" 13 | encoder = { pattern = "[{l}]\t{d} - {t} - {m}{n}" } -------------------------------------------------------------------------------- /node-plugins/edgelink-nodes-dummy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "edgelink-nodes-dummy" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | async-trait.workspace = true 8 | tokio-util.workspace = true 9 | log.workspace = true 10 | inventory.workspace = true 11 | edgelink-core = { path = "../../crates/core", default-features = false, features = [ 12 | "core", 13 | ] } 14 | edgelink-macro = { path = "../../crates/macro" } 15 | -------------------------------------------------------------------------------- /node-plugins/edgelink-nodes-dummy/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use async_trait::*; 4 | use tokio_util::sync::CancellationToken; 5 | 6 | use edgelink_core::runtime::context::*; 7 | use edgelink_core::runtime::flow::*; 8 | use edgelink_core::runtime::model::json::*; 9 | use edgelink_core::runtime::model::*; 10 | use edgelink_core::runtime::nodes::*; 11 | use edgelink_core::Result; 12 | use edgelink_macro::*; 13 | 14 | #[flow_node("dummy")] 15 | struct DummyNode { 16 | base: FlowNode, 17 | } 18 | 19 | impl DummyNode { 20 | fn build(_flow: &Flow, state: FlowNode, _config: &RedFlowNodeConfig) -> Result> { 21 | let node = DummyNode { base: state }; 22 | Ok(Box::new(node)) 23 | } 24 | } 25 | 26 | #[async_trait] 27 | impl FlowNodeBehavior for DummyNode { 28 | fn get_node(&self) -> &FlowNode { 29 | &self.base 30 | } 31 | 32 | async fn run(self: Arc, stop_token: CancellationToken) { 33 | while !stop_token.is_cancelled() { 34 | let cancel = stop_token.child_token(); 35 | with_uow(self.as_ref(), cancel.child_token(), |node, msg| async move { 36 | node.fan_out_one(Envelope { port: 0, msg }, cancel.child_token()).await?; 37 | Ok(()) 38 | }) 39 | .await; 40 | } 41 | } 42 | } 43 | 44 | pub fn foo() {} 45 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --it 3 | asyncio_mode = strict 4 | asyncio_default_fixture_loop_scope = function 5 | timeout = 3 -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_field_init_shorthand = true 2 | max_width = 120 3 | use_small_heuristics = "Max" 4 | edition = "2021" -------------------------------------------------------------------------------- /scripts/cross-unittest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import subprocess 4 | import sys 5 | import json 6 | import os 7 | import argparse 8 | 9 | parser = argparse.ArgumentParser(description="Run test binaries using qemu-*.") 10 | parser.add_argument("qemu", help="The QEMU command (qemu-arm)") 11 | parser.add_argument("toolchain_prefix", help="The toolchain prefix (e.g., arm-linux-gnueabihf)") 12 | parser.add_argument("cargo_output", help="The path to the cargo-output.json file") 13 | args = parser.parse_args() 14 | 15 | qemu_cmd = args.qemu 16 | toolchain_prefix = args.toolchain_prefix 17 | cargo_output_path = args.cargo_output 18 | 19 | try: 20 | with open(cargo_output_path, 'r') as f: 21 | cargo_output = [json.loads(line) for line in f] 22 | except FileNotFoundError: 23 | print(f"Error: {cargo_output_path} not found. Please run cargo test first.") 24 | sys.exit(1) 25 | 26 | test_binaries = [ 27 | entry['executable'] for entry in cargo_output 28 | if entry.get('profile', {}).get('test') == True 29 | ] 30 | 31 | if not test_binaries: 32 | print("No test binaries found.") 33 | sys.exit(0) 34 | 35 | exit_code = 0 36 | 37 | for test_binary in test_binaries: 38 | print(f"Running test binary: {test_binary}") 39 | 40 | result = subprocess.run([qemu_cmd, "-L", f"/usr/{toolchain_prefix}", test_binary]) 41 | 42 | if result.returncode != 0: 43 | print(f"Test failed: {test_binary}") 44 | exit_code = 1 45 | 46 | sys.exit(exit_code) 47 | 48 | -------------------------------------------------------------------------------- /scripts/gen-red-node-specs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$1" ] || [ -z "$2" ]; then 4 | echo "Usage: $0 " 5 | exit 1 6 | fi 7 | 8 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 9 | TARGET_DIR="$1" 10 | OUTPUT_DIR="$2" 11 | 12 | cd "$TARGET_DIR" || { echo "Directory $TARGET_DIR does not exist."; exit 1; } 13 | 14 | OUTPUT_FILE="$OUTPUT_DIR/nodered-nodes-specs.json" 15 | 16 | mocha "test/nodes/**/*_spec.js" --dry-run --reporter=json --exit --reporter-options output="$OUTPUT_FILE" 17 | 18 | cd - > /dev/null -------------------------------------------------------------------------------- /scripts/load-pymod-test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import asyncio 4 | import importlib.util 5 | 6 | 7 | current_script_path = os.path.abspath(__file__) 8 | current_directory = os.path.dirname(current_script_path) 9 | target_directory = os.path.normpath(os.path.join(current_directory, '..', 'target', 'debug')) 10 | module_path = os.path.join(target_directory, "libedgelink_pymod.so") 11 | spec = importlib.util.spec_from_file_location("edgelink_pymod", module_path) 12 | edgelink = importlib.util.module_from_spec(spec) 13 | spec.loader.exec_module(edgelink) 14 | 15 | 16 | async def main(): 17 | 18 | flows_json = [ 19 | { "id": "100", "type": "tab", "label": "Flow 1" }, 20 | { "id": "1", "z": "100", "type": "test-once" } 21 | ] 22 | msgs = [ 23 | ("1", {"payload": "Hello World!"}) 24 | ] 25 | config = {} 26 | #fn run_flows_once<'a>(py: Python<'a>, _expected_msgs: usize, _timeout: f64, py_json: &'a PyAny) -> PyResult<&'a PyAny> { 27 | msgs = await edgelink.run_flows_once(1, 0.2, flows_json, msgs, config) 28 | print(msgs) 29 | 30 | # should sleep for 1s 31 | asyncio.run(main()) 32 | -------------------------------------------------------------------------------- /scripts/specs_diff.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "category": "common nodes", 4 | "nodes": [ 5 | [ 6 | "inject", 7 | "nodes/common/test_inject_node.py", 8 | "test/nodes/core/common/20-inject_spec.js" 9 | ], 10 | [ 11 | "catch", 12 | "nodes/common/test_catch_node.py", 13 | "test/nodes/core/common/25-catch_spec.js" 14 | ], 15 | [ 16 | "link", 17 | "nodes/common/test_link_nodes.py", 18 | "test/nodes/core/common/60-link_spec.js" 19 | ] 20 | ] 21 | }, 22 | { 23 | "category": "function nodes", 24 | "nodes": [ 25 | [ 26 | "function", 27 | "nodes/function/test_function_node.py", 28 | "test/nodes/core/function/10-function_spec.js" 29 | ], 30 | [ 31 | "switch", 32 | "nodes/function/test_switch_node.py", 33 | "test/nodes/core/function/10-switch_spec.js" 34 | ], 35 | [ 36 | "change", 37 | "nodes/function/test_change_node.py", 38 | "test/nodes/core/function/15-change_spec.js" 39 | ], 40 | [ 41 | "range", 42 | "nodes/function/test_range_node.py", 43 | "test/nodes/core/function/16-range_spec.js" 44 | ], 45 | [ 46 | "template", 47 | "nodes/function/test_template_node.py", 48 | "test/nodes/core/function/80-template_spec.js" 49 | ], 50 | [ 51 | "delay", 52 | "nodes/function/test_delay_node.py", 53 | "test/nodes/core/function/89-delay_spec.js" 54 | ], 55 | [ 56 | "trigger", 57 | "nodes/function/test_trigger_node.py", 58 | "test/nodes/core/function/89-trigger_spec.js" 59 | ], 60 | [ 61 | "rbe", 62 | "nodes/function/test_rbe_node.py", 63 | "test/nodes/core/function/rbe_spec.js" 64 | ] 65 | ] 66 | }, 67 | { 68 | "category": "subflow", 69 | "nodes": [ 70 | [ 71 | "subflow", 72 | "nodes/test_subflow.py", 73 | "test/nodes/subflow/subflow_spec.js" 74 | ] 75 | ] 76 | } 77 | ] -------------------------------------------------------------------------------- /scripts/specs_diff.py: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | 3 | import re 4 | import argparse 5 | import ast 6 | import difflib 7 | import os 8 | import json 9 | import shutil 10 | import subprocess 11 | import tempfile 12 | import pytest 13 | import io 14 | import contextlib 15 | 16 | from colorama import init as colorama_init 17 | from colorama import Fore 18 | from colorama import Style 19 | 20 | 21 | _SCRIPT_PATH = os.path.abspath(__file__) 22 | _SCRIPT_DIR = os.path.dirname(_SCRIPT_PATH) 23 | TESTS_DIR = os.path.join(_SCRIPT_DIR, '..', "tests") 24 | 25 | JS_IT_PATTERN = re.compile(r"""^\s*it\s*\(\s*(['"].*?['"]+)\s*,\s*""") 26 | PY_IT_PATTERN = re.compile(r"""\@.*it\s*\(\s*(['"].*?['"]+)\s*\)\s*""") 27 | 28 | def load_json(json_path): 29 | with open(json_path, 'r') as fp: 30 | return json.load(fp) 31 | 32 | 33 | def extract_it_strings_js(red_dir, file_path) -> list[str]: 34 | specs = [] 35 | with tempfile.NamedTemporaryFile(delete=True) as report_file: 36 | original_cwd = os.getcwd() 37 | os.chdir(red_dir) 38 | try: 39 | result = subprocess.run([ 40 | 'mocha', 41 | file_path, "--dry-run", "--reporter=json", "--exit", 42 | "--reporter-options", f"output={report_file.name}" 43 | ]) 44 | report = load_json(report_file.name) 45 | for test in report['tests']: 46 | specs.append(test['fullTitle'].rstrip()) 47 | finally: 48 | os.chdir(original_cwd) 49 | 50 | return specs 51 | 52 | 53 | def extract_it_strings_py(file_path) -> list[str]: 54 | specs = [] 55 | with tempfile.NamedTemporaryFile(delete=True) as report_file: 56 | output_capture = io.StringIO() 57 | with contextlib.redirect_stdout(output_capture), contextlib.redirect_stderr(output_capture): 58 | pytest.main(["-q", "--co", "--disable-warnings", "-p", "no:skip", 59 | "--json-report", f"--json-report-file={report_file.name}", file_path]) 60 | report = load_json(report_file.name) 61 | for coll in report['collectors']: 62 | for result in coll['result']: 63 | if "title" in result: 64 | specs.append(result['fullTitle'].rstrip()) 65 | return specs 66 | 67 | 68 | def read_json() -> list[list]: 69 | json_path = os.path.join(_SCRIPT_DIR, 'specs_diff.json') 70 | with open(json_path, 'r', encoding='utf-8') as file: 71 | json_text = file.read() 72 | return json.loads(json_text) 73 | 74 | 75 | def print_sep(text=''): 76 | terminal_size = shutil.get_terminal_size() 77 | filled_text = text.ljust(terminal_size.columns, '-') 78 | print(filled_text) 79 | 80 | def print_subtitle(text=''): 81 | terminal_size = shutil.get_terminal_size() 82 | filled_text = text.ljust(terminal_size.columns, '.') 83 | print(filled_text) 84 | 85 | def generate_markdown_table(rows): 86 | headers = ["Status", "Spec Test"] 87 | table = "| " + " | ".join(headers) + " |\n" 88 | table += "| " + " | ".join(["---"] * len(headers)) + " |\n" 89 | for row in rows: 90 | if row[0] == " ": 91 | table += f"| :white_check_mark: | ~~{row[1]}~~ |\n" 92 | if row[0] == "-": 93 | table += f"| :x: | **{row[1]}** |\n" 94 | return table 95 | 96 | 97 | if __name__ == "__main__": 98 | parser = argparse.ArgumentParser( 99 | description="Scan a .js file, extract lines containing it('arbitrary text') or it(\"arbitrary text\"), and print the text with a four-digit number prefix.") 100 | parser.add_argument('NR_PATH', type=str, 101 | help="Path to the directory of Node-RED") 102 | parser.add_argument('-o', "--output", type=str, default=None, 103 | help="The output path to a Markdown file") 104 | args = parser.parse_args() 105 | 106 | colorama_init() 107 | 108 | categories = read_json() 109 | 110 | markdown = [] 111 | 112 | total_js_count = 0 113 | total_py_count = 0 114 | for cat in categories: 115 | md_cat = {"category": cat["category"], "nodes": []} 116 | for triple in cat["nodes"]: 117 | md_node = {"node": triple[0], "specs": []} 118 | py_path = os.path.join(os.path.normpath(os.path.join(TESTS_DIR, triple[1]))) 119 | js_path = os.path.join(args.NR_PATH, triple[2]) 120 | js_specs = extract_it_strings_js(args.NR_PATH, js_path) 121 | js_specs.sort() 122 | py_specs = extract_it_strings_py(py_path) 123 | py_specs.sort() 124 | 125 | diff = difflib.Differ().compare(js_specs, py_specs) 126 | # differences = [line for line in diff if line.startswith( 127 | # '-') or line.startswith('+')] 128 | differences = [line for line in diff] 129 | total_js_count += len(js_specs) 130 | total_py_count += len(py_specs) 131 | if len(py_specs) >= len(js_specs): 132 | print_subtitle( 133 | f'''{Fore.GREEN}* [✓]{Style.RESET_ALL} "{triple[0]}" ({len(py_specs)}/{len(js_specs)}) ''') 134 | else: 135 | print_subtitle( 136 | f'''{Fore.RED}* [×]{Style.RESET_ALL} "{triple[0]}" {Fore.RED}({len(py_specs)}/{len(js_specs)}){Style.RESET_ALL} ''') 137 | for s in differences: 138 | if s[0] == '-': 139 | print(f'\t{Fore.RED}{s[0]} It: {Style.RESET_ALL}{s[2:]}') 140 | md_node["specs"].append(["-", s[2:]]) 141 | elif s[0] == '+': 142 | print(f'\t{Fore.GREEN}{s[0]} It: {Style.RESET_ALL}{s[2:]}') 143 | md_node["specs"].append(["+", s[2:]]) 144 | elif s[0] == '?': 145 | print(f'\t{Fore.YELLOW}{s[0]} It: {Style.RESET_ALL}{s[2:]}') 146 | md_node["specs"].append(["*", s[2:]]) 147 | else: 148 | print(f'\t{Style.DIM}{s[0]} It: {s[2:]}{Style.RESET_ALL}') 149 | md_node["specs"].append([" ", s[2:]]) 150 | md_node["specs"].sort(key=lambda x: x[0]) 151 | md_cat["nodes"].append(md_node) 152 | markdown.append(md_cat) 153 | 154 | print_sep("") 155 | print("Total:") 156 | print(f"JS specs:\t{str(total_js_count).rjust(8)}") 157 | print(f"Python specs:\t{str(total_py_count).rjust(8)}") 158 | pc = "{:>{}.1%}".format(total_py_count * 1.0 / total_js_count, 8) 159 | print(f"Percent:\t{pc}") 160 | 161 | if args.output != None: 162 | with open(args.output, 'w', encoding='utf-8') as md_file: 163 | md_file.write("# Node-RED Spec Tests Diff\n") 164 | md_file.write( 165 | "This file is automatically generated by the script `test/specs_diff.py` to compare the specification " + 166 | "compliance of EdgeLink and Node-RED nodes. \n") 167 | for cat in markdown: 168 | md_file.write(f"## {cat['category']}\n") 169 | for node in cat["nodes"]: 170 | md_file.write(f"### {node['node']}\n") 171 | md_file.write(generate_markdown_table(node["specs"])) 172 | 173 | if total_py_count < total_js_count: 174 | exit(-1) 175 | else: 176 | exit(0) 177 | -------------------------------------------------------------------------------- /scripts/udp_server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | class UDPEchoServerProtocol: 4 | def connection_made(self, transport): 5 | self.transport = transport 6 | 7 | def datagram_received(self, data, addr): 8 | message = data.decode('utf-8') 9 | print(f"Received message from {addr}:\n{message}\n") 10 | #print(f"Send {message} to {addr}") 11 | #self.transport.sendto(data, addr) 12 | 13 | async def main(): 14 | loop = asyncio.get_running_loop() 15 | print("UDP Echo server started on 127.0.0.1:9981") 16 | 17 | # 创建一个 UDP 端点并指定协议 18 | transport, protocol = await loop.create_datagram_endpoint( 19 | lambda: UDPEchoServerProtocol(), 20 | local_addr=('127.0.0.1', 9981) 21 | ) 22 | 23 | try: 24 | # 保持运行状态,直到接收到 Ctrl+C 25 | while True: 26 | await asyncio.sleep(3600) 27 | except KeyboardInterrupt: 28 | print("\nUDP Echo server is shutting down...") 29 | finally: 30 | transport.close() 31 | 32 | if __name__ == "__main__": 33 | asyncio.run(main()) 34 | -------------------------------------------------------------------------------- /src/cliargs.rs: -------------------------------------------------------------------------------- 1 | // use clap::{Parser, Subcommand}; 2 | use clap::Parser; 3 | 4 | const LONG_ABOUT: &str = r#" 5 | EdgeLink Daemon Program 6 | 7 | EdgeLink is a Node-RED compatible run-time engine implemented in Rust. 8 | 9 | Copyright (C) 2023-TODAY Li Wei and contributors. All rights reserved. 10 | 11 | For more information, visit the website: https://github.com/oldrev/edgelink 12 | "#; 13 | 14 | #[derive(Parser, Debug, Clone)] 15 | #[command(version, about, author, long_about=LONG_ABOUT, color=clap::ColorChoice::Always)] 16 | pub struct CliArgs { 17 | /// Path of the 'flows.json' file. 18 | #[clap(default_value_t = default_flows_path(), conflicts_with = "stdin")] 19 | pub flows_path: String, 20 | 21 | /// Home directory of EdgeLink, default is `~/.edgelink` 22 | #[arg(long)] 23 | pub home: Option, 24 | 25 | /// Path of the log configuration file. 26 | #[arg(short, long)] 27 | pub log_path: Option, 28 | 29 | /// Use verbose output, '0' means quiet, no output printed to stdout. 30 | #[arg(short, long, default_value_t = 2)] 31 | pub verbose: usize, 32 | 33 | /// Read flows JSON from stdin. 34 | #[arg(long, default_value_t = false)] 35 | pub stdin: bool, 36 | 37 | /// Set the running environment in 'dev' or 'prod', default is `dev` 38 | #[arg(long)] 39 | pub env: Option, 40 | } 41 | 42 | fn default_flows_path() -> String { 43 | dirs_next::home_dir() 44 | .expect("Can not found the $HOME dir!!!") 45 | .join(".node-red") 46 | .join("flows.json") 47 | .to_string_lossy() 48 | .to_string() 49 | } 50 | -------------------------------------------------------------------------------- /src/consts.rs: -------------------------------------------------------------------------------- 1 | pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); 2 | pub const GIT_HASH: &str = env!("EDGELINK_BUILD_GIT_HASH"); 3 | -------------------------------------------------------------------------------- /src/logging.rs: -------------------------------------------------------------------------------- 1 | use crate::CliArgs; 2 | 3 | pub(crate) fn log_init(elargs: &CliArgs) { 4 | if let Some(ref log_path) = elargs.log_path { 5 | log4rs::init_file(log_path, Default::default()).unwrap(); 6 | } else { 7 | let stderr = log4rs::append::console::ConsoleAppender::builder() 8 | .target(log4rs::append::console::Target::Stderr) 9 | .encoder(Box::new(log4rs::encode::pattern::PatternEncoder::new("[{h({l})}]\t{m}{n}"))) 10 | .build(); 11 | 12 | let level = match elargs.verbose { 13 | 0 => log::LevelFilter::Off, 14 | 1 => log::LevelFilter::Warn, 15 | 2 => log::LevelFilter::Info, 16 | 3 => log::LevelFilter::Debug, 17 | _ => log::LevelFilter::Trace, 18 | }; 19 | 20 | let config = log4rs::Config::builder() 21 | .appender(log4rs::config::Appender::builder().build("stderr", Box::new(stderr))) 22 | .build(log4rs::config::Root::builder().appender("stderr").build(level)) 23 | .unwrap(); // TODO FIXME 24 | 25 | let _ = log4rs::init_config(config).unwrap(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import json 3 | import pytest_jsonreport.serialize 4 | 5 | 6 | def _make_collectitem(item): 7 | """Return JSON-serializable collection item.""" 8 | json_item = { 9 | 'nodeid': item.nodeid, 10 | 'type': item.__class__.__name__, 11 | } 12 | for marker in item.iter_markers(): 13 | if isinstance(marker, pytest.Mark): 14 | if marker.name == "it": 15 | it = marker.args[0] if marker.args else None 16 | if it != None: 17 | path = [x[1] for x in _parent_marks(item)] + [it] 18 | json_item["title"] = it 19 | json_item["fullTitle"] = " ".join(path) 20 | try: 21 | location = item.location 22 | except AttributeError: 23 | pass 24 | else: 25 | json_item['lineno'] = location[1] 26 | return json_item 27 | 28 | pytest_jsonreport.serialize.make_collectitem = _make_collectitem 29 | 30 | 31 | 32 | """ 33 | 34 | #@pytest.hookimpl(trylast=True, hookwrapper=True, optionalhook=True) 35 | @pytest.hookimpl(trylast=True, optionalhook=True) 36 | def pytest_runtest_makereport(item, call): 37 | outcome = yield 38 | report = outcome.get_result() 39 | 40 | # only add this during call instead of during any stage 41 | print(call.when) 42 | if call.when == 'call': 43 | test_info = { 44 | "title": None, 45 | "fullTitle": None 46 | } 47 | 48 | # Extract it and describe markers 49 | #print(_parent_marks(item)) 50 | for marker in item.iter_markers(): 51 | if isinstance(marker, pytest.Mark): 52 | if marker.name == "it": 53 | test_info["title"] = marker.args[0] if marker.args else None 54 | path = [x[1] for x in _parent_marks(item)] + [test_info["title"]] 55 | test_info["fullTitle"] = " ".join(path) 56 | #report.test_metadata = test_info 57 | print(test_info) 58 | return test_info 59 | else: 60 | return {} 61 | """ 62 | 63 | def _parent_marks(item): 64 | markers = [] 65 | for m in item.iter_markers(): 66 | if m.name in ("describe", "context"): 67 | try: 68 | markers.append((m.name, m.args[0])) 69 | except IndexError: 70 | pass 71 | return list(reversed(markers)) -------------------------------------------------------------------------------- /tests/data/flows.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "dee0d1b0cfd62a6c", 4 | "type": "tab", 5 | "label": "Flow 1", 6 | "disabled": false, 7 | "info": "", 8 | "env": [] 9 | }, 10 | { 11 | "id": "bf843d35fe7cf583", 12 | "type": "inject", 13 | "z": "dee0d1b0cfd62a6c", 14 | "name": "", 15 | "props": [ 16 | { 17 | "p": "payload" 18 | }, 19 | { 20 | "p": "topic", 21 | "vt": "str" 22 | } 23 | ], 24 | "repeat": "", 25 | "crontab": "", 26 | "once": false, 27 | "onceDelay": 0.1, 28 | "topic": "", 29 | "payload": "", 30 | "payloadType": "date", 31 | "x": 350, 32 | "y": 180, 33 | "wires": [ 34 | [ 35 | "c75509302b4b8fc1" 36 | ] 37 | ] 38 | }, 39 | { 40 | "id": "c75509302b4b8fc1", 41 | "type": "debug", 42 | "z": "dee0d1b0cfd62a6c", 43 | "name": "debug 1", 44 | "active": true, 45 | "tosidebar": true, 46 | "console": false, 47 | "tostatus": false, 48 | "complete": "false", 49 | "statusVal": "", 50 | "statusType": "auto", 51 | "x": 540, 52 | "y": 180, 53 | "wires": [] 54 | } 55 | ] -------------------------------------------------------------------------------- /tests/home/edgelinkd.toml: -------------------------------------------------------------------------------- 1 | [runtime] 2 | 3 | [runtime.engine] 4 | 5 | [runtime.context] 6 | default = "memory" 7 | 8 | [runtime.context.stores] 9 | memory = { provider = "memory" } 10 | memory0 = { provider = "memory" } 11 | memory1 = { provider = "memory" } 12 | memory2 = { provider = "memory" } 13 | 14 | 15 | [runtime.flow] 16 | node_msg_queue_capacity = 16 -------------------------------------------------------------------------------- /tests/nodes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldrev/edgelink/21aaed943fdc9ba14b4919ce6296cf4eb1713690/tests/nodes/__init__.py -------------------------------------------------------------------------------- /tests/nodes/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldrev/edgelink/21aaed943fdc9ba14b4919ce6296cf4eb1713690/tests/nodes/common/__init__.py -------------------------------------------------------------------------------- /tests/nodes/common/test_catch_node.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | import pytest 4 | 5 | from tests import * 6 | 7 | @pytest.mark.describe('catch Node') 8 | class TestCatchNode: 9 | 10 | @pytest.mark.asyncio 11 | @pytest.mark.it('should output a message when called') 12 | async def test_it_should_output_a_message_when_called(self): 13 | flows = [ 14 | {"id": "100", "type": "tab"}, # flow 1 15 | {"id": "1", "z": "100", "type": "function", "name":"func1", "func": 'throw new Error("big error");', "wires": []}, 16 | {"id": "2", "z": "100", "type": "catch", "wires": [["3"]]}, 17 | {"id": "3", "z": "100", "type": "test-once"}, 18 | ] 19 | injections = [ 20 | {"nid": "1", "msg": {"payload": "foo"}} 21 | ] 22 | msgs = await run_flow_with_msgs_ntimes(flows, injections, 1) 23 | assert msgs[0]["payload"] == "foo" 24 | assert "error" in msgs[0] 25 | assert "source" in msgs[0]["error"] 26 | assert "message" in msgs[0]["error"] 27 | assert msgs[0]["error"]["source"]["type"] == "function" 28 | assert msgs[0]["error"]["source"]["name"] == "func1" 29 | 30 | -------------------------------------------------------------------------------- /tests/nodes/common/test_junction_node.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | import pytest 4 | 5 | from tests import * 6 | 7 | @pytest.mark.describe('junction node') 8 | class TestJunctionNode: 9 | 10 | @pytest.mark.asyncio 11 | @pytest.mark.it('junction node should work') 12 | async def test_0001(self): 13 | flows = [ 14 | {"id": "100", "type": "tab"}, # flow 1 15 | {"id": "1", "z": "100", "type": "junction", "wires": [["2"]]}, 16 | {"id": "2", "z": "100", "type": "test-once"}, 17 | ] 18 | injections = [ 19 | {"nid": "1", "msg": {"payload": "foo"}} 20 | ] 21 | msgs = await run_flow_with_msgs_ntimes(flows, injections, 1) 22 | assert msgs[0]["payload"] == "foo" 23 | 24 | -------------------------------------------------------------------------------- /tests/nodes/function/test_delay_node.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pytest 3 | import pytest_asyncio 4 | import asyncio 5 | import os 6 | 7 | from tests import * 8 | 9 | @pytest.mark.describe('delay Node') 10 | class TestDelayNode: 11 | pass -------------------------------------------------------------------------------- /tests/nodes/function/test_range_node.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pytest 3 | import pytest_asyncio 4 | import asyncio 5 | import os 6 | 7 | from tests import * 8 | 9 | async def _generic_range_test(action, minin, maxin, minout, maxout, round, a_payload, expected_result): 10 | node = {"type": "range", "minin": minin, "maxin": maxin, "minout": minout, 11 | "maxout": maxout, "action": action, "round": round} 12 | msgs = await run_with_single_node_ntimes( 13 | 'num', 14 | isinstance(a_payload, str) and a_payload or json.dumps(a_payload), 15 | node, 1, once=True, topic='t1') 16 | assert msgs[0]['payload'] == expected_result 17 | 18 | 19 | @pytest.mark.describe('range Node') 20 | class TestRangeNode: 21 | 22 | @pytest.mark.asyncio 23 | @pytest.mark.it('''ranges numbers up tenfold''') 24 | async def test_0001(self): 25 | await _generic_range_test("scale", 0, 100, 0, 1000, False, 50, 500) 26 | 27 | @pytest.mark.asyncio 28 | @pytest.mark.it('''ranges numbers down such as centimetres to metres''') 29 | async def test_0002(self): 30 | await _generic_range_test("scale", 0, 100, 0, 1, False, 55, 0.55) 31 | 32 | @pytest.mark.asyncio 33 | @pytest.mark.it('''wraps numbers down say for degree/rotation reading 1/2''') 34 | async def test_0003(self): 35 | # 1/2 around wrap => "one and a half turns" 36 | await _generic_range_test("roll", 0, 10, 0, 360, True, 15, 180) 37 | 38 | @pytest.mark.asyncio 39 | @pytest.mark.it('''wraps numbers around say for degree/rotation reading 1/3''') 40 | async def test_0004(self): 41 | # 1/3 around wrap => "one and a third turns" 42 | await _generic_range_test("roll", 0, 10, 0, 360, True, 13.3333, 120) 43 | 44 | @pytest.mark.asyncio 45 | @pytest.mark.it('''wraps numbers around say for degree/rotation reading 1/4''') 46 | async def test_0005(self): 47 | # 1/4 around wrap => "one and a quarter turns" 48 | await _generic_range_test("roll", 0, 10, 0, 360, True, 12.5, 90) 49 | 50 | @pytest.mark.asyncio 51 | @pytest.mark.it('''wraps numbers down say for degree/rotation reading 1/4''') 52 | async def test_0006(self): 53 | # 1/4 backwards wrap => "one and a quarter turns backwards" 54 | await _generic_range_test("roll", 0, 10, 0, 360, True, -12.5, 270) 55 | 56 | @pytest.mark.asyncio 57 | @pytest.mark.it('''wraps numbers around say for degree/rotation reading 0''') 58 | async def test_0007(self): 59 | await _generic_range_test("roll", 0, 10, 0, 360, True, -10, 0) 60 | 61 | @pytest.mark.asyncio 62 | @pytest.mark.it('''clamps numbers within a range - over max''') 63 | async def test_0008(self): 64 | '''clamps numbers within a range - over max''' 65 | await _generic_range_test("clamp", 0, 10, 0, 1000, False, 111, 1000) 66 | 67 | @pytest.mark.asyncio 68 | @pytest.mark.it('''clamps numbers within a range - below min''') 69 | async def test_0009(self): 70 | await _generic_range_test("clamp", 0, 10, 0, 1000, False, -1, 0) 71 | 72 | @pytest.mark.asyncio 73 | @pytest.mark.it('''drops msg if in drop mode and input outside range''') 74 | async def test_0010(self): 75 | node = { 76 | "type": "range", "minin": 2, "maxin": 8, "minout": 20, "maxout": 80, 77 | "action": "drop", "round": True 78 | } 79 | injections = [ 80 | {'payload': "1.0"}, 81 | {'payload': "9.0"}, 82 | {'payload': "5.0"}, 83 | ] 84 | msgs = await run_single_node_with_msgs_ntimes(node, injections, 1) 85 | assert msgs[0]['payload'] == 50 86 | 87 | @pytest.mark.asyncio 88 | @pytest.mark.it('''just passes on msg if payload not present''') 89 | async def test_0011(self): 90 | node = { 91 | "type": "range", "minin": 0, "maxin": 100, "minout": 0, "maxout": 100, 92 | "action": "scale", "round": True 93 | } 94 | injections = [{}] 95 | msgs = await run_single_node_with_msgs_ntimes(node, injections, 1) 96 | assert 'payload' not in msgs[0] 97 | 98 | @pytest.mark.asyncio 99 | @pytest.mark.it('reports if input is not a number') 100 | async def test_it_reports_if_input_is_not_a_number(self): 101 | flows = [ 102 | {"id": "100", "type": "tab"}, # flow 1 103 | {"id": "1", "z": "100", "type": "range", 104 | "minin": 0, "maxin": 0, "minout": 0, "maxout": 0, "action": "scale", "round": True, }, 105 | {"id": "2", "z": "100", "type": "catch", "wires": [["3"]]}, 106 | {"id": "3", "z": "100", "type": "test-once"}, 107 | ] 108 | injections = [ 109 | {"nid": "1", "msg": {"payload": "NOT A NUMBER"}} 110 | ] 111 | msgs = await run_flow_with_msgs_ntimes(flows, injections, 1) 112 | assert 'error' in msgs[0] 113 | -------------------------------------------------------------------------------- /tests/nodes/function/test_switch_node.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pytest 3 | import pytest_asyncio 4 | import asyncio 5 | import os 6 | 7 | from tests import * 8 | 9 | @pytest.mark.describe('switch Node') 10 | class TestSwitchNode: 11 | pass -------------------------------------------------------------------------------- /tests/nodes/function/test_template_node.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pytest 3 | import pytest_asyncio 4 | import asyncio 5 | import os 6 | 7 | from tests import * 8 | 9 | @pytest.mark.describe('template Node') 10 | class TestTemplateNode: 11 | pass -------------------------------------------------------------------------------- /tests/nodes/function/test_trigger_node.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pytest 3 | import pytest_asyncio 4 | import asyncio 5 | import os 6 | 7 | from tests import * 8 | 9 | @pytest.mark.describe('trigger Node') 10 | class TestTriggerNode: 11 | pass -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest==8.3.2 2 | pytest-asyncio==0.24.0 3 | pytest-timeout==2.3.1 4 | pytest-it==0.1.5 5 | pytest-json-report==1.5.0 6 | colorama==0.4.6 --------------------------------------------------------------------------------