├── .github └── workflows │ └── golangci-lint.yml ├── .gitignore ├── .golangci.yml ├── .travis.yml ├── LICENSE ├── README.md ├── component.go ├── component_test.go ├── components_for_test.go ├── factory.go ├── factory_test.go ├── go.mod ├── graph.go ├── graph_connect.go ├── graph_connect_test.go ├── graph_iip.go ├── graph_iip_test.go ├── graph_ports.go ├── graph_ports_test.go ├── graph_test.go ├── loader.go ├── loader_test.go ├── protocol.go ├── runtime.go ├── runtime_test.go └── test_codecov.sh /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - master 8 | - lint 9 | pull_request: 10 | jobs: 11 | golangci: 12 | name: lint 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: golangci-lint 17 | uses: golangci/golangci-lint-action@v2 18 | with: 19 | # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. 20 | version: v1.31 21 | # Optional: working directory, useful for monorepos 22 | # working-directory: somedir 23 | # Optional: golangci-lint command line arguments. 24 | # args: --issues-exit-code=0 25 | # Optional: show only new issues if it's a pull request. The default value is `false`. 26 | # only-new-issues: true 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | *.cov 14 | 15 | .vscode 16 | 17 | # Editor files 18 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # This file contains all available configuration options 2 | # with their default values. 3 | 4 | # options for analysis running 5 | run: 6 | # default concurrency is a available CPU number 7 | concurrency: 4 8 | 9 | # timeout for analysis, e.g. 30s, 5m, default is 1m 10 | timeout: 1m 11 | 12 | # exit code when at least one issue was found, default is 1 13 | issues-exit-code: 1 14 | 15 | # include test files or not, default is true 16 | tests: true 17 | 18 | # list of build tags, all linters use it. Default is empty list. 19 | build-tags: 20 | 21 | # which dirs to skip: issues from them won't be reported; 22 | # can use regexp here: generated.*, regexp is applied on full path; 23 | # default value is empty list, but default dirs are skipped independently 24 | # from this option's value (see skip-dirs-use-default). 25 | # "/" will be replaced by current OS file path separator to properly work 26 | # on Windows. 27 | skip-dirs: 28 | 29 | # default is true. Enables skipping of directories: 30 | # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ 31 | skip-dirs-use-default: true 32 | 33 | # which files to skip: they will be analyzed, but issues from them 34 | # won't be reported. Default value is empty list, but there is 35 | # no need to include all autogenerated files, we confidently recognize 36 | # autogenerated files. If it's not please let us know. 37 | # "/" will be replaced by current OS file path separator to properly work 38 | # on Windows. 39 | skip-files: 40 | 41 | # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": 42 | # If invoked with -mod=readonly, the go command is disallowed from the implicit 43 | # automatic updating of go.mod described above. Instead, it fails when any changes 44 | # to go.mod are needed. This setting is most useful to check that go.mod does 45 | # not need updates, such as in a continuous integration and testing system. 46 | # If invoked with -mod=vendor, the go command assumes that the vendor 47 | # directory holds the correct copies of dependencies and ignores 48 | # the dependency descriptions in go.mod. 49 | # modules-download-mode: readonly|release|vendor 50 | 51 | # Allow multiple parallel golangci-lint instances running. 52 | # If false (default) - golangci-lint acquires file lock on start. 53 | allow-parallel-runners: false 54 | 55 | # output configuration options 56 | output: 57 | # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" 58 | format: colored-line-number 59 | 60 | # print lines of code with issue, default is true 61 | print-issued-lines: true 62 | 63 | # print linter name in the end of issue text, default is true 64 | print-linter-name: true 65 | 66 | # make issues output unique by line, default is true 67 | uniq-by-line: true 68 | 69 | # add a prefix to the output file references; default is no prefix 70 | path-prefix: "" 71 | 72 | # all available settings of specific linters 73 | linters-settings: 74 | dogsled: 75 | # checks assignments with too many blank identifiers; default is 2 76 | max-blank-identifiers: 2 77 | dupl: 78 | # tokens count to trigger issue, 150 by default 79 | threshold: 100 80 | errcheck: 81 | # report about not checking of errors in type assertions: `a := b.(MyStruct)`; 82 | # default is false: such cases aren't reported by default. 83 | check-type-assertions: false 84 | 85 | # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; 86 | # default is false: such cases aren't reported by default. 87 | check-blank: false 88 | 89 | # [deprecated] comma-separated list of pairs of the form pkg:regex 90 | # the regex is used to ignore names within pkg. (default "fmt:.*"). 91 | # see https://github.com/kisielk/errcheck#the-deprecated-method for details 92 | ignore: fmt:.*,io/ioutil:^Read.* 93 | 94 | # path to a file containing a list of functions to exclude from checking 95 | # see https://github.com/kisielk/errcheck#excluding-functions for details 96 | # exclude: /path/to/file.txt 97 | exhaustive: 98 | # indicates that switch statements are to be considered exhaustive if a 99 | # 'default' case is present, even if all enum members aren't listed in the 100 | # switch 101 | default-signifies-exhaustive: false 102 | funlen: 103 | lines: 60 104 | statements: 40 105 | gci: 106 | # put imports beginning with prefix after 3rd-party packages; 107 | # only support one prefix 108 | # if not set, use goimports.local-prefixes 109 | local-prefixes: github.com/org/project 110 | gocognit: 111 | # minimal code complexity to report, 30 by default (but we recommend 10-20) 112 | min-complexity: 10 113 | nestif: 114 | # minimal complexity of if statements to report, 5 by default 115 | min-complexity: 4 116 | goconst: 117 | # minimal length of string constant, 3 by default 118 | min-len: 3 119 | # minimal occurrences count to trigger, 3 by default 120 | min-occurrences: 3 121 | gocritic: 122 | # Which checks should be enabled; can't be combined with 'disabled-checks'; 123 | # See https://go-critic.github.io/overview#checks-overview 124 | # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run` 125 | # By default list of stable checks is used. 126 | enabled-checks: 127 | # - rangeValCopy 128 | 129 | # Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty 130 | disabled-checks: 131 | - regexpMust 132 | 133 | # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks. 134 | # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". 135 | enabled-tags: 136 | - performance 137 | disabled-tags: 138 | - experimental 139 | 140 | settings: # settings passed to gocritic 141 | captLocal: # must be valid enabled check name 142 | paramsOnly: true 143 | rangeValCopy: 144 | sizeThreshold: 32 145 | gocyclo: 146 | # minimal code complexity to report, 30 by default (but we recommend 10-20) 147 | min-complexity: 10 148 | godot: 149 | # check all top-level comments, not only declarations 150 | check-all: false 151 | godox: 152 | # report any comments starting with keywords, this is useful for TODO or FIXME comments that 153 | # might be left in the code accidentally and should be resolved before merging 154 | # keywords: # default keywords are TODO, BUG, and FIXME, these can be overwritten by this setting 155 | # - NOTE 156 | # - OPTIMIZE # marks code that should be optimized before merging 157 | # - HACK # marks hack-arounds that should be removed before merging 158 | gofmt: 159 | # simplify code: gofmt with `-s` option, true by default 160 | simplify: true 161 | goheader: 162 | values: 163 | const: 164 | # define here const type values in format k:v, for example: 165 | # YEAR: 2020 166 | # COMPANY: MY COMPANY 167 | regexp: 168 | # define here regexp type values, for example 169 | # AUTHOR: .*@mycompany\.com 170 | template: 171 | # put here copyright header template for source code files, for example: 172 | # {{ AUTHOR }} {{ COMPANY }} {{ YEAR }} 173 | # SPDX-License-Identifier: Apache-2.0 174 | # 175 | # Licensed under the Apache License, Version 2.0 (the "License"); 176 | # you may not use this file except in compliance with the License. 177 | # You may obtain a copy of the License at: 178 | # 179 | # http://www.apache.org/licenses/LICENSE-2.0 180 | # 181 | # Unless required by applicable law or agreed to in writing, software 182 | # distributed under the License is distributed on an "AS IS" BASIS, 183 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 184 | # See the License for the specific language governing permissions and 185 | # limitations under the License. 186 | template-path: 187 | # also as alternative of directive 'template' you may put the path to file with the template source 188 | goimports: 189 | # put imports beginning with prefix after 3rd-party packages; 190 | # it's a comma-separated list of prefixes 191 | local-prefixes: github.com/org/project 192 | golint: 193 | # minimal confidence for issues, default is 0.8 194 | min-confidence: 0.8 195 | gomnd: 196 | settings: 197 | mnd: 198 | # the list of enabled checks, see https://github.com/tommy-muehle/go-mnd/#checks for description. 199 | checks: argument,case,condition,operation,return,assign 200 | gomodguard: 201 | allowed: 202 | # List of allowed modules 203 | modules: 204 | # - gopkg.in/yaml.v2 205 | # List of allowed module domains 206 | domains: 207 | # - golang.org 208 | blocked: 209 | modules: # List of blocked modules 210 | # - github.com/uudashr/go-module: # Blocked module 211 | # recommendations: # Recommended modules that should be used instead (Optional) 212 | # - golang.org/x/mod 213 | # reason: "`mod` is the official go.mod parser library." # Reason why the recommended module should be used (Optional) 214 | # List of blocked module version constraints 215 | versions: 216 | # - github.com/mitchellh/go-homedir: # Blocked module with version constraint 217 | # version: "< 1.1.0" # Version constraint, see https://github.com/Masterminds/semver#basic-comparisons 218 | # reason: "testing if blocked version constraint works." # Reason why the version constraint exists. (Optional) 219 | govet: 220 | # report about shadowed variables 221 | check-shadowing: true 222 | 223 | # settings per analyzer 224 | settings: 225 | printf: # analyzer name, run `go tool vet help` to see all analyzers 226 | funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer 227 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof 228 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf 229 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf 230 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf 231 | 232 | # enable or disable analyzers by name 233 | enable: 234 | - atomicalign 235 | enable-all: false 236 | disable: 237 | - shadow 238 | disable-all: false 239 | depguard: 240 | list-type: blacklist 241 | include-go-root: false 242 | packages: 243 | - github.com/sirupsen/logrus 244 | packages-with-error-message: 245 | # specify an error message to output when a blacklisted package is used 246 | - github.com/sirupsen/logrus: "logging is allowed only by logutils.Log" 247 | lll: 248 | # max line length, lines longer will be reported. Default is 120. 249 | # '\t' is counted as 1 character by default, and can be changed with the tab-width option 250 | line-length: 160 251 | # tab width in spaces. Default to 1. 252 | tab-width: 4 253 | maligned: 254 | # print struct with more effective memory layout or not, false by default 255 | suggest-new: true 256 | misspell: 257 | # Correct spellings using locale preferences for US or UK. 258 | # Default is to use a neutral variety of English. 259 | # Setting locale to US will correct the British spelling of 'colour' to 'color'. 260 | locale: US 261 | ignore-words: 262 | - someword 263 | nakedret: 264 | # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 265 | max-func-lines: 30 266 | prealloc: 267 | # XXX: we don't recommend using this linter before doing performance profiling. 268 | # For most programs usage of prealloc will be a premature optimization. 269 | 270 | # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. 271 | # True by default. 272 | simple: true 273 | range-loops: true # Report preallocation suggestions on range loops, true by default 274 | for-loops: false # Report preallocation suggestions on for loops, false by default 275 | nolintlint: 276 | # Enable to ensure that nolint directives are all used. Default is true. 277 | allow-unused: false 278 | # Disable to ensure that nolint directives don't have a leading space. Default is true. 279 | allow-leading-space: true 280 | # Exclude following linters from requiring an explanation. Default is []. 281 | allow-no-explanation: [] 282 | # Enable to require an explanation of nonzero length after each nolint directive. Default is false. 283 | require-explanation: true 284 | # Enable to require nolint directives to mention the specific linter being suppressed. Default is false. 285 | require-specific: true 286 | rowserrcheck: 287 | packages: 288 | - github.com/jmoiron/sqlx 289 | testpackage: 290 | # regexp pattern to skip files 291 | skip-regexp: (export|internal)_test\.go 292 | unparam: 293 | # Inspect exported functions, default is false. Set to true if no external program/library imports your code. 294 | # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: 295 | # if it's called for subdir of a project it can't find external interfaces. All text editor integrations 296 | # with golangci-lint call it on a directory with the changed file. 297 | check-exported: false 298 | unused: 299 | # treat code as a program (not a library) and report unused exported identifiers; default is false. 300 | # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: 301 | # if it's called for subdir of a project it can't find funcs usages. All text editor integrations 302 | # with golangci-lint call it on a directory with the changed file. 303 | check-exported: false 304 | whitespace: 305 | multi-if: false # Enforces newlines (or comments) after every multi-line if statement 306 | multi-func: false # Enforces newlines (or comments) after every multi-line function signature 307 | wsl: 308 | # If true append is only allowed to be cuddled if appending value is 309 | # matching variables, fields or types on line above. Default is true. 310 | strict-append: true 311 | # Allow calls and assignments to be cuddled as long as the lines have any 312 | # matching variables, fields or types. Default is true. 313 | allow-assign-and-call: true 314 | # Allow multiline assignments to be cuddled. Default is true. 315 | allow-multiline-assign: true 316 | # Allow declarations (var) to be cuddled. 317 | allow-cuddle-declarations: false 318 | # Allow trailing comments in ending of blocks 319 | allow-trailing-comment: false 320 | # Force newlines in end of case at this limit (0 = never). 321 | force-case-trailing-whitespace: 0 322 | # Force cuddling of err checks with err var assignment 323 | force-err-cuddling: false 324 | # Allow leading comments to be separated with empty liens 325 | allow-separated-leading-comment: false 326 | gofumpt: 327 | # Choose whether or not to use the extra rules that are disabled 328 | # by default 329 | extra-rules: false 330 | errorlint: 331 | # Report non-wrapping error creation using fmt.Errorf 332 | errorf: true 333 | 334 | # The custom section can be used to define linter plugins to be loaded at runtime. See README doc 335 | # for more info. 336 | custom: 337 | # Each custom linter should have a unique name. 338 | example: 339 | # The path to the plugin *.so. Can be absolute or local. Required for each custom linter 340 | # path: /path/to/example.so 341 | # The description of the linter. Optional, just for documentation purposes. 342 | # description: This is an example usage of a plugin linter. 343 | # Intended to point to the repo location of the linter. Optional, just for documentation purposes. 344 | # original-url: github.com/golangci/example-linter 345 | 346 | linters: 347 | enable: 348 | - megacheck 349 | - govet 350 | - gocritic 351 | - godot 352 | - gofumpt 353 | - gosec 354 | - golint 355 | - gochecknoglobals 356 | - gochecknoinits 357 | - gofmt 358 | - asciicheck 359 | - bodyclose 360 | - depguard 361 | - dogsled 362 | - errcheck 363 | - exhaustive 364 | - exportloopref 365 | - funlen 366 | - gci 367 | - goconst 368 | - goheader 369 | - goimports 370 | - gomodguard 371 | - goprintffuncname 372 | - ineffassign 373 | - interfacer 374 | - lll 375 | - maligned 376 | - misspell 377 | - nakedret 378 | - noctx 379 | - nolintlint 380 | - prealloc 381 | - rowserrcheck 382 | - scopelint 383 | - sqlclosecheck 384 | - staticcheck 385 | - stylecheck 386 | - unconvert 387 | - unparam 388 | - whitespace 389 | - wsl 390 | disable: 391 | - deadcode 392 | - dupl 393 | - gocognit 394 | - gocyclo 395 | - godox 396 | - goerr113 397 | - gomnd 398 | - nestif 399 | - nlreturn 400 | - structcheck 401 | - testpackage 402 | - unused 403 | 404 | disable-all: false 405 | # presets: 406 | # - bugs 407 | # - unused 408 | fast: false 409 | 410 | issues: 411 | # List of regexps of issue texts to exclude, empty list by default. 412 | # But independently from this option we use default exclude patterns, 413 | # it can be disabled by `exclude-use-default: false`. To list all 414 | # excluded by default patterns execute `golangci-lint run --help` 415 | exclude: 416 | # - abcdef 417 | 418 | # Excluding configuration per-path, per-linter, per-text and per-source 419 | exclude-rules: 420 | # Exclude some linters from running on tests files. 421 | - path: _test\.go 422 | linters: 423 | # - gocyclo 424 | - errcheck 425 | # - dupl 426 | - gosec 427 | 428 | # Exclude known linters from partially hard-vendored code, 429 | # which is impossible to exclude via "nolint" comments. 430 | # - path: internal/hmac/ 431 | # text: "weak cryptographic primitive" 432 | # linters: 433 | # - gosec 434 | 435 | # Exclude some staticcheck messages 436 | # - linters: 437 | # - staticcheck 438 | # text: "SA9003:" 439 | 440 | # Exclude lll issues for long lines with go:generate 441 | - linters: 442 | - lll 443 | source: "^//go:generate " 444 | 445 | # Independently from option `exclude` we use default exclude patterns, 446 | # it can be disabled by this option. To list all 447 | # excluded by default patterns execute `golangci-lint run --help`. 448 | # Default value for this option is true. 449 | exclude-use-default: false 450 | 451 | # The default value is false. If set to true exclude and exclude-rules 452 | # regular expressions become case sensitive. 453 | exclude-case-sensitive: false 454 | 455 | # The list of ids of default excludes to include or disable. By default it's empty. 456 | include: 457 | - EXC0002 # disable excluding of issues about comments from golint 458 | 459 | # Maximum issues count per one linter. Set to 0 to disable. Default is 50. 460 | max-issues-per-linter: 0 461 | 462 | # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. 463 | max-same-issues: 0 464 | 465 | # Show only new issues: if there are unstaged changes or untracked files, 466 | # only those changes are analyzed, else only changes in HEAD~ are analyzed. 467 | # It's a super-useful option for integration of golangci-lint into existing 468 | # large codebase. It's not practical to fix all existing issues at the moment 469 | # of integration: much better don't allow issues in new code. 470 | # Default is false. 471 | new: false 472 | 473 | # Show only new issues created after git revision `REV` 474 | # new-from-rev: REV 475 | # Show only new issues created in git patch with set file path. 476 | # new-from-patch: path/to/patch/file 477 | 478 | severity: 479 | # Default value is empty string. 480 | # Set the default severity for issues. If severity rules are defined and the issues 481 | # do not match or no severity is provided to the rule this will be the default 482 | # severity applied. Severities should match the supported severity names of the 483 | # selected out format. 484 | # - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity 485 | # - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#severity 486 | # - Github: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message 487 | default-severity: error 488 | 489 | # The default value is false. 490 | # If set to true severity-rules regular expressions become case sensitive. 491 | case-sensitive: false 492 | 493 | # Default value is empty list. 494 | # When a list of severity rules are provided, severity information will be added to lint 495 | # issues. Severity rules have the same filtering capability as exclude rules except you 496 | # are allowed to specify one matcher per severity rule. 497 | # Only affects out formats that support setting severity information. 498 | rules: 499 | - linters: 500 | - dupl 501 | severity: info 502 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.x" 5 | 6 | script: 7 | - go test -v -race 8 | - ./test_codecov.sh 9 | 10 | after_success: 11 | - bash <(curl -s https://codecov.io/bash) 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2019 Vladimir Sibirov 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoFlow - Dataflow and Flow-based programming library for Go (golang) 2 | 3 | [![Build Status](https://travis-ci.com/trustmaster/goflow.svg?branch=master)](https://travis-ci.com/trustmaster/goflow) [![codecov](https://codecov.io/gh/trustmaster/goflow/branch/master/graph/badge.svg)](https://codecov.io/gh/trustmaster/goflow) 4 | 5 | 6 | ### _Status of this branch (WIP)_ 7 | 8 | _Warning: you are currently on v1 branch of GoFlow. v1 is a revisit and refactoring of the original GoFlow code which remained almost unchanged for 7 years. This branch is deep **in progress**, no stability guaranteed. API also may change._ 9 | 10 | - _[More information on v1](https://github.com/trustmaster/goflow/issues/49)_ 11 | - _[Take me back to v0](https://github.com/trustmaster/goflow/tree/v0)_ 12 | 13 | _If your code depends on the old implementation, you can build it using [release 0.1](https://github.com/trustmaster/goflow/releases/tag/0.1)._ 14 | 15 | -- 16 | 17 | GoFlow is a lean and opinionated implementation of [Flow-based programming](http://en.wikipedia.org/wiki/Flow-based_programming) in Go that aims at designing applications as graphs of components which react to data that flows through the graph. 18 | 19 | The main properties of the proposed model are: 20 | 21 | * Concurrent - graph nodes run in parallel. 22 | * Structural - applications are described as components, their ports and connections between them. 23 | * Reactive/active - system's behavior is how components react to events or how they handle their lifecycle. 24 | * Asynchronous/synchronous - there is no determined order in which events happen, unless you demand for such order. 25 | * Isolated - sharing is done by communication, state is not shared. 26 | 27 | ## Getting started 28 | 29 | If you don't have the Go compiler installed, read the official [Go install guide](http://golang.org/doc/install). 30 | 31 | Use go tool to install the package in your packages tree: 32 | 33 | ``` 34 | go get github.com/trustmaster/goflow 35 | ``` 36 | 37 | Then you can use it in import section of your Go programs: 38 | 39 | ```go 40 | import "github.com/trustmaster/goflow" 41 | ``` 42 | 43 | ## Basic Example 44 | 45 | Below there is a listing of a simple program running a network of two processes. 46 | 47 | ![Greeter example diagram](http://flowbased.wdfiles.com/local--files/goflow/goflow-hello.png) 48 | 49 | This first one generates greetings for given names, the second one prints them on screen. It demonstrates how components and graphs are defined and how they are embedded into the main program. 50 | 51 | ```go 52 | package main 53 | 54 | import ( 55 | "fmt" 56 | "github.com/trustmaster/goflow" 57 | ) 58 | 59 | // Greeter sends greetings 60 | type Greeter struct { 61 | Name <-chan string // input port 62 | Res chan<- string // output port 63 | } 64 | 65 | // Process incoming data 66 | func (c *Greeter) Process() { 67 | // Keep reading incoming packets 68 | for name := range c.Name { 69 | greeting := fmt.Sprintf("Hello, %s!", name) 70 | // Send the greeting to the output port 71 | c.Res <- greeting 72 | } 73 | } 74 | 75 | // Printer prints its input on screen 76 | type Printer struct { 77 | Line <-chan string // inport 78 | } 79 | 80 | // Process prints a line when it gets it 81 | func (c *Printer) Process() { 82 | for line := range c.Line { 83 | fmt.Println(line) 84 | } 85 | } 86 | 87 | // NewGreetingApp defines the app graph 88 | func NewGreetingApp() *goflow.Graph { 89 | n := goflow.NewGraph() 90 | // Add processes to the network 91 | n.Add("greeter", new(Greeter)) 92 | n.Add("printer", new(Printer)) 93 | // Connect them with a channel 94 | n.Connect("greeter", "Res", "printer", "Line") 95 | // Our net has 1 inport mapped to greeter.Name 96 | n.MapInPort("In", "greeter", "Name") 97 | return n 98 | } 99 | 100 | func main() { 101 | // Create the network 102 | net := NewGreetingApp() 103 | // We need a channel to talk to it 104 | in := make(chan string) 105 | net.SetInPort("In", in) 106 | // Run the net 107 | wait := goflow.Run(net) 108 | // Now we can send some names and see what happens 109 | in <- "John" 110 | in <- "Boris" 111 | in <- "Hanna" 112 | // Send end of input 113 | close(in) 114 | // Wait until the net has completed its job 115 | <-wait 116 | } 117 | ``` 118 | 119 | Looks a bit heavy for such a simple task but FBP is aimed at a bit more complex things than just printing on screen. So in more complex an realistic examples the infractructure pays the price. 120 | 121 | You probably have one question left even after reading the comments in code: why do we need to wait for the finish signal? This is because flow-based world is asynchronous and while you expect things to happen in the same sequence as they are in main(), during runtime they don't necessarily follow the same order and the application might terminate before the network has done its job. To avoid this confusion we listen for a signal on network's `wait` channel which is sent when the network finishes its job. 122 | 123 | ## Terminology 124 | 125 | Here are some Flow-based programming terms used in GoFlow: 126 | 127 | * Component - the basic element that processes data. Its structure consists of input and output ports and state fields. Its behavior is the set of event handlers. In OOP terms Component is a Class. 128 | * Connection - a link between 2 ports in the graph. In Go it is a channel of specific type. 129 | * Graph - components and connections between them, forming a higher level entity. Graphs may represent composite components or entire applications. In OOP terms Graph is a Class. 130 | * Network - is a Graph instance running in memory. In OOP terms a Network is an object of Graph class. 131 | * Port - is a property of a Component or Graph through which it communicates with the outer world. There are input ports (Inports) and output ports (Outports). For GoFlow components it is a channel field. 132 | * Process - is a Component instance running in memory. In OOP terms a Process is an object of Component class. 133 | 134 | More terms can be found in [Flow-based Wiki Terms](https://github.com/flowbased/flowbased.org/wiki/Terminology) and [FBP wiki](http://www.jpaulmorrison.com/cgi-bin/wiki.pl?action=index). 135 | 136 | ## Documentation 137 | 138 | ### Contents 139 | 140 | 1. [Components](https://github.com/trustmaster/goflow/wiki/Components) 141 | 1. [Ports and Events](https://github.com/trustmaster/goflow/wiki/Components#ports-and-events) 142 | 2. [Process](https://github.com/trustmaster/goflow/wiki/Components#process) 143 | 3. [State](https://github.com/trustmaster/goflow/wiki/Components#state) 144 | 2. [Graphs](https://github.com/trustmaster/goflow/wiki/Graphs) 145 | 1. [Structure definition](https://github.com/trustmaster/goflow/wiki/Graphs#structure-definition) 146 | 2. [Behavior](https://github.com/trustmaster/goflow/wiki/Graphs#behavior) 147 | 148 | ### Package docs 149 | 150 | Documentation for the flow package can be accessed using standard godoc tool, e.g. 151 | 152 | ``` 153 | godoc github.com/trustmaster/goflow 154 | ``` 155 | 156 | ## Links 157 | 158 | Here are related projects and resources: 159 | 160 | * [Flowbased.org](https://github.com/flowbased/flowbased.org/wiki), specifications and recommendations for FBP systems. 161 | * [J. Paul Morrison's Flow-Based Programming](https://jpaulm.github.io/fbp/index.html), the origin of FBP, [JavaFBP](https://github.com/jpaulm/javafbp), [C#FBP](https://github.com/jpaulm/csharpfbp) and [DrawFBP](https://github.com/jpaulm/drawfbp) diagramming tool. 162 | * [NoFlo](http://noflojs.org/), FBP for JavaScript and Node.js 163 | * [Go](http://golang.org/), the Go programming language 164 | 165 | ## TODO 166 | 167 | * Integration with NoFlo-UI/Flowhub (in progress) 168 | * Distributed networks via TCP/IP and UDP 169 | * Reflection and monitoring of networks 170 | -------------------------------------------------------------------------------- /component.go: -------------------------------------------------------------------------------- 1 | package goflow 2 | 3 | // Component is a unit that can start a process. 4 | type Component interface { 5 | Process() 6 | } 7 | 8 | // Done notifies that the process is finished. 9 | type Done struct{} 10 | 11 | // Wait is a channel signaling of a completion. 12 | type Wait chan Done 13 | 14 | // Run the component process. 15 | func Run(c Component) Wait { 16 | wait := make(Wait) 17 | 18 | go func() { 19 | c.Process() 20 | wait <- Done{} 21 | }() 22 | 23 | return wait 24 | } 25 | -------------------------------------------------------------------------------- /component_test.go: -------------------------------------------------------------------------------- 1 | package goflow 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // Test a simple component that runs only once. 8 | func TestSimpleComponent(t *testing.T) { 9 | in := make(chan int) 10 | out := make(chan int) 11 | c := &doubleOnce{ 12 | in, 13 | out, 14 | } 15 | 16 | wait := Run(c) 17 | 18 | in <- 12 19 | 20 | res := <-out 21 | 22 | if res != 24 { 23 | t.Errorf("%d != %d", res, 24) 24 | } 25 | 26 | <-wait 27 | } 28 | 29 | // Test a simple long running component with one input. 30 | func TestSimpleLongRunningComponent(t *testing.T) { 31 | data := map[int]int{ 32 | 12: 24, 33 | 7: 14, 34 | 400: 800, 35 | } 36 | in := make(chan int) 37 | out := make(chan int) 38 | c := &doubler{ 39 | in, 40 | out, 41 | } 42 | 43 | wait := Run(c) 44 | 45 | for src, expected := range data { 46 | in <- src 47 | 48 | actual := <-out 49 | 50 | if actual != expected { 51 | t.Errorf("%d != %d", actual, expected) 52 | } 53 | } 54 | 55 | // We have to close input for the process to finish 56 | close(in) 57 | <-wait 58 | } 59 | 60 | func TestComponentWithTwoInputs(t *testing.T) { 61 | op1 := []int{3, 5, 92, 28} 62 | op2 := []int{38, 94, 4, 9} 63 | sums := []int{41, 99, 96, 37} 64 | 65 | in1 := make(chan int) 66 | in2 := make(chan int) 67 | out := make(chan int) 68 | c := &adder{in1, in2, out} 69 | 70 | wait := Run(c) 71 | 72 | go func() { 73 | for _, n := range op1 { 74 | in1 <- n 75 | } 76 | 77 | close(in1) 78 | }() 79 | 80 | go func() { 81 | for _, n := range op2 { 82 | in2 <- n 83 | } 84 | 85 | close(in2) 86 | }() 87 | 88 | for i := 0; i < len(sums); i++ { 89 | actual := <-out 90 | expected := sums[i] 91 | 92 | if actual != expected { 93 | t.Errorf("%d != %d", actual, expected) 94 | } 95 | } 96 | 97 | <-wait 98 | } 99 | -------------------------------------------------------------------------------- /components_for_test.go: -------------------------------------------------------------------------------- 1 | package goflow 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // doubler doubles its input. 8 | type doubler struct { 9 | In <-chan int 10 | Out chan<- int 11 | } 12 | 13 | func (c *doubler) Process() { 14 | for i := range c.In { 15 | c.Out <- 2 * i 16 | } 17 | } 18 | 19 | // doubleOnce is a non-resident version of doubler. 20 | type doubleOnce struct { 21 | In <-chan int 22 | Out chan<- int 23 | } 24 | 25 | func (c *doubleOnce) Process() { 26 | i := <-c.In 27 | c.Out <- 2 * i 28 | } 29 | 30 | // A component with two inputs and one output. 31 | type adder struct { 32 | Op1 <-chan int 33 | Op2 <-chan int 34 | Sum chan<- int 35 | } 36 | 37 | func (c *adder) Process() { 38 | op1Buf := make([]int, 0, 10) 39 | op2Buf := make([]int, 0, 10) 40 | addOp := func(op int, buf, otherBuf *[]int) { 41 | if len(*otherBuf) > 0 { 42 | otherOp := (*otherBuf)[0] 43 | *otherBuf = (*otherBuf)[1:] 44 | c.Sum <- (op + otherOp) 45 | } else { 46 | *buf = append(*buf, op) 47 | } 48 | } 49 | 50 | for c.Op1 != nil || c.Op2 != nil { 51 | select { 52 | case op1, ok := <-c.Op1: 53 | if !ok { 54 | c.Op1 = nil 55 | break 56 | } 57 | 58 | addOp(op1, &op1Buf, &op2Buf) 59 | case op2, ok := <-c.Op2: 60 | if !ok { 61 | c.Op2 = nil 62 | break 63 | } 64 | 65 | addOp(op2, &op2Buf, &op1Buf) 66 | } 67 | } 68 | } 69 | 70 | // echo passes input to the output. 71 | type echo struct { 72 | In <-chan int 73 | Out chan<- int 74 | } 75 | 76 | func (c *echo) Process() { 77 | for i := range c.In { 78 | c.Out <- i 79 | } 80 | } 81 | 82 | // repeater repeats an input string a given number of times. 83 | type repeater struct { 84 | Word <-chan string 85 | Times <-chan int 86 | 87 | Words chan<- string 88 | } 89 | 90 | func (c *repeater) Process() { 91 | times := 0 92 | word := "" 93 | 94 | for c.Times != nil || c.Word != nil { 95 | select { 96 | case t, ok := <-c.Times: 97 | if !ok { 98 | c.Times = nil 99 | break 100 | } 101 | 102 | times = t 103 | c.repeat(word, times) 104 | case w, ok := <-c.Word: 105 | if !ok { 106 | c.Word = nil 107 | break 108 | } 109 | 110 | word = w 111 | c.repeat(word, times) 112 | } 113 | } 114 | } 115 | 116 | func (c *repeater) repeat(word string, times int) { 117 | if word == "" || times <= 0 { 118 | return 119 | } 120 | 121 | for i := 0; i < times; i++ { 122 | c.Words <- word 123 | } 124 | } 125 | 126 | // router routes input map port to output. 127 | type router struct { 128 | In map[string]<-chan int 129 | Out map[string]chan<- int 130 | } 131 | 132 | // Process routes incoming packets to the output by sending them to the same 133 | // outport key as the inport key they arrived at. 134 | func (c *router) Process() { 135 | wg := new(sync.WaitGroup) 136 | 137 | for k, ch := range c.In { 138 | k := k 139 | ch := ch 140 | 141 | wg.Add(1) 142 | 143 | go func() { 144 | for n := range ch { 145 | c.Out[k] <- n 146 | } 147 | 148 | close(c.Out[k]) 149 | wg.Done() 150 | }() 151 | } 152 | 153 | wg.Wait() 154 | } 155 | 156 | // irouter routes input array port to output. 157 | type irouter struct { 158 | In [](<-chan int) 159 | Out [](chan<- int) 160 | } 161 | 162 | // Process routes incoming packets to the output by sending them to the same 163 | // outport key as the inport key they arrived at. 164 | func (c *irouter) Process() { 165 | wg := new(sync.WaitGroup) 166 | 167 | for k, ch := range c.In { 168 | k := k 169 | ch := ch 170 | 171 | wg.Add(1) 172 | 173 | go func() { 174 | for n := range ch { 175 | c.Out[k] <- n 176 | } 177 | 178 | close(c.Out[k]) 179 | wg.Done() 180 | }() 181 | } 182 | 183 | wg.Wait() 184 | } 185 | 186 | func RegisterTestComponents(f *Factory) error { 187 | f.Register("echo", func() (interface{}, error) { 188 | return new(echo), nil 189 | }) 190 | f.Annotate("echo", Annotation{ 191 | Description: "Passes an int from in to out without changing it", 192 | Icon: "arrow-right", 193 | }) 194 | f.Register("doubler", func() (interface{}, error) { 195 | return new(doubler), nil 196 | }) 197 | f.Annotate("doubler", Annotation{ 198 | Description: "Doubles its input", 199 | Icon: "times-circle", 200 | }) 201 | f.Register("repeater", func() (interface{}, error) { 202 | return new(repeater), nil 203 | }) 204 | f.Annotate("repeater", Annotation{ 205 | Description: "Repeats Word given numer of Times", 206 | Icon: "times-circle", 207 | }) 208 | f.Register("adder", func() (interface{}, error) { 209 | return new(adder), nil 210 | }) 211 | f.Annotate("adder", Annotation{ 212 | Description: "Sums integers coming to its inports", 213 | Icon: "plus-circle", 214 | }) 215 | 216 | return nil 217 | } 218 | -------------------------------------------------------------------------------- /factory.go: -------------------------------------------------------------------------------- 1 | package goflow 2 | 3 | import "fmt" 4 | 5 | // Factory registers components and creates their instances at run-time. 6 | // Not safe for concurrent use. 7 | type Factory struct { 8 | registry map[string]registryEntry // map by the component name 9 | } 10 | 11 | // registryEntry contains runtime information about a component. 12 | type registryEntry struct { 13 | constructor Constructor // Function for creating component instances at run-time 14 | info ComponentInfo // Run-time component description 15 | } 16 | 17 | // Constructor is used to create a component instance at run-time. 18 | type Constructor func() (interface{}, error) 19 | 20 | // NewFactory creates a new component Factory instance. 21 | func NewFactory() *Factory { 22 | return &Factory{ 23 | registry: make(map[string]registryEntry), 24 | } 25 | } 26 | 27 | // Register registers a component so that it can be instantiated at run-time. 28 | func (f *Factory) Register(componentName string, constructor Constructor) error { 29 | if _, exists := f.registry[componentName]; exists { 30 | return fmt.Errorf("registry error: component '%s' already registered", componentName) 31 | } 32 | 33 | f.registry[componentName] = registryEntry{ 34 | constructor: constructor, 35 | info: ComponentInfo{ 36 | Name: componentName, 37 | }, 38 | } 39 | 40 | return nil 41 | } 42 | 43 | // Annotation provides reference information about a component to graph designers and operators. 44 | type Annotation struct { 45 | Description string // Description of what the component does 46 | Icon string // Icon name in Font Awesome used for visualization 47 | } 48 | 49 | // Annotate adds human-readable documentation for a component to the runtime. 50 | func (f *Factory) Annotate(componentName string, annotation Annotation) error { 51 | entry, exists := f.registry[componentName] 52 | if !exists { 53 | return fmt.Errorf("registry annotation error: component '%s' is not registered", componentName) 54 | } 55 | 56 | entry.info.Description = annotation.Description 57 | entry.info.Icon = annotation.Icon 58 | f.registry[componentName] = entry 59 | 60 | return nil 61 | } 62 | 63 | // Unregister removes a component with a given name from the component registry and returns true 64 | // or returns false if no such component is registered. 65 | func (f *Factory) Unregister(componentName string) error { 66 | if _, exists := f.registry[componentName]; !exists { 67 | return fmt.Errorf("registry error: component '%s' is not registered", componentName) 68 | } 69 | 70 | delete(f.registry, componentName) 71 | 72 | return nil 73 | } 74 | 75 | // Create creates a new instance of a component registered under a specific name. 76 | func (f *Factory) Create(componentName string) (interface{}, error) { 77 | info, exists := f.registry[componentName] 78 | if !exists { 79 | return nil, fmt.Errorf("factory error: component '%s' does not exist", componentName) 80 | } 81 | 82 | return info.constructor() 83 | } 84 | 85 | // // UpdateComponentInfo extracts run-time information about a 86 | // // component and its ports. It is called when an FBP protocol client 87 | // // requests component information. 88 | // func (f *Factory) UpdateComponentInfo(componentName string) bool { 89 | // component, exists := f.registry[componentName] 90 | // if !exists { 91 | // return false 92 | // } 93 | // // A component instance is required to reflect its type and ports 94 | // instance := component.Constructor() 95 | 96 | // component.Info.Name = componentName 97 | 98 | // portMap, isGraph := instance.(portMapper) 99 | // if isGraph { 100 | // // Is a subgraph 101 | // component.Info.Subgraph = true 102 | // inPorts := portMap.listInPorts() 103 | // component.Info.InPorts = make([]PortInfo, len(inPorts)) 104 | // for key, value := range inPorts { 105 | // if value.info.Id == "" { 106 | // value.info.Id = key 107 | // } 108 | // if value.info.Type == "" { 109 | // value.info.Type = value.channel.Elem().Type().Name() 110 | // } 111 | // component.Info.InPorts = append(component.Info.InPorts, value.info) 112 | // } 113 | // outPorts := portMap.listOutPorts() 114 | // component.Info.OutPorts = make([]PortInfo, len(outPorts)) 115 | // for key, value := range outPorts { 116 | // if value.info.Id == "" { 117 | // value.info.Id = key 118 | // } 119 | // if value.info.Type == "" { 120 | // value.info.Type = value.channel.Elem().Type().Name() 121 | // } 122 | // component.Info.OutPorts = append(component.Info.OutPorts, value.info) 123 | // } 124 | // } else { 125 | // // Is a component 126 | // component.Info.Subgraph = false 127 | // v := reflect.ValueOf(instance).Elem() 128 | // t := v.Type() 129 | // component.Info.InPorts = make([]PortInfo, t.NumField()) 130 | // component.Info.OutPorts = make([]PortInfo, t.NumField()) 131 | // for i := 0; i < t.NumField(); i++ { 132 | // f := t.Field(i) 133 | // if f.Type.Kind() == reflect.Chan { 134 | // required := true 135 | // if f.Tag.Get("required") == "false" { 136 | // required = false 137 | // } 138 | // addressable := false 139 | // if f.Tag.Get("addressable") == "true" { 140 | // addressable = true 141 | // } 142 | // port := PortInfo{ 143 | // Id: f.Name, 144 | // Type: f.Type.Name(), 145 | // Description: f.Tag.Get("description"), 146 | // Addressable: addressable, 147 | // Required: required, 148 | // } 149 | // if (f.Type.ChanDir() & reflect.RecvDir) != 0 { 150 | // component.Info.InPorts = append(component.Info.InPorts, port) 151 | // } else if (f.Type.ChanDir() & reflect.SendDir) != 0 { 152 | // component.Info.OutPorts = append(component.Info.OutPorts, port) 153 | // } 154 | // } 155 | // } 156 | // } 157 | // return true 158 | // } 159 | -------------------------------------------------------------------------------- /factory_test.go: -------------------------------------------------------------------------------- 1 | package goflow 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestFactoryCreate(t *testing.T) { 8 | f := NewFactory() 9 | 10 | if err := RegisterTestComponents(f); err != nil { 11 | t.Error(err) 12 | return 13 | } 14 | 15 | instance, err := f.Create("echo") 16 | if err != nil { 17 | t.Error(err) 18 | return 19 | } 20 | 21 | c, ok := instance.(Component) 22 | 23 | if !ok { 24 | t.Errorf("%+v is not a Component", c) 25 | return 26 | } 27 | 28 | _, err = f.Create("notfound") 29 | if err == nil { 30 | t.Errorf("Expected an error") 31 | } 32 | } 33 | 34 | func TestFactoryRegistration(t *testing.T) { 35 | f := NewFactory() 36 | 37 | if err := RegisterTestComponents(f); err != nil { 38 | t.Error(err) 39 | return 40 | } 41 | 42 | err := f.Register("echo", func() (interface{}, error) { 43 | return new(echo), nil 44 | }) 45 | if err == nil { 46 | t.Errorf("Expected an error") 47 | return 48 | } 49 | 50 | err = f.Annotate("notfound", Annotation{}) 51 | if err == nil { 52 | t.Errorf("Expected an error") 53 | return 54 | } 55 | 56 | err = f.Unregister("echo") 57 | if err != nil { 58 | t.Error(err) 59 | return 60 | } 61 | 62 | err = f.Unregister("echo") 63 | if err == nil { 64 | t.Errorf("Expected an error") 65 | return 66 | } 67 | } 68 | 69 | func TestFactoryGraph(t *testing.T) { 70 | f := NewFactory() 71 | 72 | if err := RegisterTestComponents(f); err != nil { 73 | t.Error(err) 74 | return 75 | } 76 | 77 | if err := RegisterTestGraph(f); err != nil { 78 | t.Error(err) 79 | return 80 | } 81 | 82 | n := NewGraph() 83 | 84 | if err := n.AddNew("de", "doubleEcho", f); err != nil { 85 | t.Error(err) 86 | return 87 | } 88 | 89 | if err := n.AddNew("e", "echo", f); err != nil { 90 | t.Error(err) 91 | return 92 | } 93 | 94 | if err := n.AddNew("notfound", "notfound", f); err == nil { 95 | t.Errorf("Expected an error") 96 | return 97 | } 98 | 99 | if err := n.Connect("de", "Out", "e", "In"); err != nil { 100 | t.Error(err) 101 | return 102 | } 103 | 104 | n.MapInPort("In", "de", "In") 105 | n.MapOutPort("Out", "e", "Out") 106 | 107 | testGraphWithNumberSequence(n, t) 108 | } 109 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/trustmaster/goflow 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /graph.go: -------------------------------------------------------------------------------- 1 | // Package goflow implements a dataflow and flow-based programming library for Go. 2 | package goflow 3 | 4 | import ( 5 | "fmt" 6 | "reflect" 7 | "sync" 8 | ) 9 | 10 | // GraphConfig sets up properties for a graph. 11 | type GraphConfig struct { 12 | BufferSize int 13 | } 14 | 15 | // Graph represents a graph of processes connected with packet channels. 16 | type Graph struct { 17 | conf GraphConfig // Graph configuration 18 | waitGrp *sync.WaitGroup // Wait group for a graceful termination 19 | procs map[string]interface{} // Network processes 20 | inPorts map[string]port // Map of network incoming ports to component ports 21 | outPorts map[string]port // Map of network outgoing ports to component ports 22 | connections []connection // Network graph edges (inter-process connections) 23 | chanListenersCount map[uintptr]uint // Tracks how many outports use the same channel 24 | chanListenersCountLock sync.Locker // Used to synchronize operations on the chanListenersCount map 25 | iips []iip // Initial Information Packets to be sent to the network on start 26 | } 27 | 28 | // NewGraph returns a new initialized empty graph instance. 29 | func NewGraph(config ...GraphConfig) *Graph { 30 | conf := GraphConfig{} 31 | if len(config) == 1 { 32 | conf = config[0] 33 | } 34 | 35 | return &Graph{ 36 | conf: conf, 37 | waitGrp: new(sync.WaitGroup), 38 | procs: make(map[string]interface{}), 39 | inPorts: make(map[string]port), 40 | outPorts: make(map[string]port), 41 | chanListenersCount: make(map[uintptr]uint), 42 | chanListenersCountLock: new(sync.Mutex), 43 | } 44 | } 45 | 46 | // NewDefaultGraph is a ComponentConstructor for the factory. 47 | func NewDefaultGraph() interface{} { 48 | return NewGraph() 49 | } 50 | 51 | // // Register an empty graph component in the registry 52 | // func init() { 53 | // Register("Graph", NewDefaultGraph) 54 | // Annotate("Graph", ComponentInfo{ 55 | // Description: "A clear graph", 56 | // Icon: "cogs", 57 | // }) 58 | // } 59 | 60 | // Add adds a new process with a given name to the network. 61 | func (n *Graph) Add(name string, c interface{}) error { 62 | // c should be either graph or a component 63 | _, isComponent := c.(Component) 64 | _, isGraph := c.(Graph) 65 | 66 | if !isComponent && !isGraph { 67 | return fmt.Errorf("could not add process '%s': instance is neither Component nor Graph", name) 68 | } 69 | // Add to the map of processes 70 | n.procs[name] = c 71 | 72 | return nil 73 | } 74 | 75 | // AddGraph adds a new blank graph instance to a network. That instance can 76 | // be modified then at run-time. 77 | func (n *Graph) AddGraph(name string) error { 78 | return n.Add(name, NewDefaultGraph()) 79 | } 80 | 81 | // AddNew creates a new process instance using component factory and adds it to the network. 82 | func (n *Graph) AddNew(processName string, componentName string, f *Factory) error { 83 | proc, err := f.Create(componentName) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | return n.Add(processName, proc) 89 | } 90 | 91 | // Remove deletes a process from the graph. First it stops the process if running. 92 | // Then it disconnects it from other processes and removes the connections from 93 | // the graph. Then it drops the process itself. 94 | func (n *Graph) Remove(processName string) error { 95 | if _, exists := n.procs[processName]; !exists { 96 | return fmt.Errorf("could not remove process: '%s' does not exist", processName) 97 | } 98 | 99 | delete(n.procs, processName) 100 | 101 | return nil 102 | } 103 | 104 | // // Rename changes a process name in all connections, external ports, IIPs and the 105 | // // graph itself. 106 | // func (n *Graph) Rename(processName, newName string) bool { 107 | // if _, exists := n.procs[processName]; !exists { 108 | // return false 109 | // } 110 | // if _, busy := n.procs[newName]; busy { 111 | // // New name is already taken 112 | // return false 113 | // } 114 | // for i, conn := range n.connections { 115 | // if conn.src.proc == processName { 116 | // n.connections[i].src.proc = newName 117 | // } 118 | // if conn.tgt.proc == processName { 119 | // n.connections[i].tgt.proc = newName 120 | // } 121 | // } 122 | // for key, port := range n.inPorts { 123 | // if port.proc == processName { 124 | // tmp := n.inPorts[key] 125 | // tmp.proc = newName 126 | // n.inPorts[key] = tmp 127 | // } 128 | // } 129 | // for key, port := range n.outPorts { 130 | // if port.proc == processName { 131 | // tmp := n.outPorts[key] 132 | // tmp.proc = newName 133 | // n.outPorts[key] = tmp 134 | // } 135 | // } 136 | // n.procs[newName] = n.procs[processName] 137 | // delete(n.procs, processName) 138 | // return true 139 | // } 140 | 141 | // // Get returns a node contained in the network by its name. 142 | // func (n *Graph) Get(processName string) interface{} { 143 | // if proc, ok := n.procs[processName]; ok { 144 | // return proc 145 | // } else { 146 | // panic("Process with name '" + processName + "' was not found") 147 | // } 148 | // } 149 | 150 | // // getWait returns net's wait group. 151 | // func (n *Graph) getWait() *sync.WaitGroup { 152 | // return n.waitGrp 153 | // } 154 | 155 | // Process runs the network. 156 | func (n *Graph) Process() { 157 | err := n.sendIIPs() 158 | if err != nil { 159 | // TODO provide a nicer way to handle graph errors 160 | panic(err) 161 | } 162 | 163 | for _, i := range n.procs { 164 | c, ok := i.(Component) 165 | if !ok { 166 | continue 167 | } 168 | 169 | n.waitGrp.Add(1) 170 | 171 | w := Run(c) 172 | proc := i 173 | 174 | go func() { 175 | <-w 176 | n.closeProcOuts(proc) 177 | n.waitGrp.Done() 178 | }() 179 | } 180 | 181 | n.waitGrp.Wait() 182 | } 183 | 184 | func (n *Graph) closeProcOuts(proc interface{}) { 185 | val := reflect.ValueOf(proc).Elem() 186 | for i := 0; i < val.NumField(); i++ { 187 | field := val.Field(i) 188 | fieldType := field.Type() 189 | 190 | if !(field.IsValid() && field.Kind() == reflect.Chan && field.CanSet() && 191 | fieldType.ChanDir()&reflect.SendDir != 0 && fieldType.ChanDir()&reflect.RecvDir == 0) { 192 | continue 193 | } 194 | 195 | if !field.IsNil() && n.decChanListenersCount(field) { 196 | field.Close() 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /graph_connect.go: -------------------------------------------------------------------------------- 1 | package goflow 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // address is a full port accessor including the index part. 11 | type address struct { 12 | proc string // Process name 13 | port string // Component port name 14 | key string // Port key (only for map ports) 15 | index int // Port index (only for array ports) 16 | } 17 | 18 | func (a address) String() string { 19 | if a.key != "" { 20 | return fmt.Sprintf("%s.%s[%s]", a.proc, a.port, a.key) 21 | } 22 | 23 | return fmt.Sprintf("%s.%s", a.proc, a.port) 24 | } 25 | 26 | // connection stores information about a connection within the net. 27 | type connection struct { 28 | src address 29 | tgt address 30 | channel reflect.Value 31 | buffer int 32 | } 33 | 34 | // Connect a sender to a receiver and create a channel between them using BufferSize graph configuration. 35 | // Normally such a connection is unbuffered but you can change by setting flow.DefaultBufferSize > 0 or 36 | // by using ConnectBuf() function instead. 37 | // It returns true on success or panics and returns false if error occurs. 38 | func (n *Graph) Connect(senderName, senderPort, receiverName, receiverPort string) error { 39 | return n.ConnectBuf(senderName, senderPort, receiverName, receiverPort, n.conf.BufferSize) 40 | } 41 | 42 | // ConnectBuf connects a sender to a receiver using a channel with a buffer of a given size. 43 | // It returns true on success or panics and returns false if error occurs. 44 | func (n *Graph) ConnectBuf(senderName, senderPort, receiverName, receiverPort string, bufferSize int) error { 45 | sendAddr := parseAddress(senderName, senderPort) 46 | 47 | sendPort, err := n.getProcPort(senderName, sendAddr.port, reflect.SendDir) 48 | if err != nil { 49 | return fmt.Errorf("connect: %w", err) 50 | } 51 | 52 | recvAddr := parseAddress(receiverName, receiverPort) 53 | 54 | recvPort, err := n.getProcPort(receiverName, recvAddr.port, reflect.RecvDir) 55 | if err != nil { 56 | return fmt.Errorf("connect: %w", err) 57 | } 58 | 59 | isNewChan := false // tells if a new channel will need to be created for this connection 60 | // Try to find an existing outbound channel from the same sender, 61 | // so it can be used as fan-out FIFO 62 | ch := n.findExistingChan(sendAddr, reflect.SendDir) 63 | if !ch.IsValid() || ch.IsNil() { 64 | // Then try to find an existing inbound channel to the same receiver, 65 | // so it can be used as a fan-in FIFO 66 | ch = n.findExistingChan(recvAddr, reflect.RecvDir) 67 | if ch.IsValid() && !ch.IsNil() { 68 | // Increase the number of listeners on this already used channel 69 | n.incChanListenersCount(ch) 70 | } else { 71 | isNewChan = true 72 | } 73 | } 74 | 75 | if ch, err = attachPort(sendPort, sendAddr, reflect.SendDir, ch, bufferSize); err != nil { 76 | return fmt.Errorf("connect '%s.%s': %w", senderName, senderPort, err) 77 | } 78 | 79 | if _, err = attachPort(recvPort, recvAddr, reflect.RecvDir, ch, bufferSize); err != nil { 80 | return fmt.Errorf("connect '%s.%s': %w", receiverName, receiverPort, err) 81 | } 82 | 83 | if isNewChan { 84 | // Register the first listener on a newly created channel 85 | n.incChanListenersCount(ch) 86 | } 87 | 88 | // Add connection info 89 | n.connections = append(n.connections, connection{ 90 | src: sendAddr, 91 | tgt: recvAddr, 92 | channel: ch, 93 | buffer: bufferSize, 94 | }) 95 | 96 | return nil 97 | } 98 | 99 | // getProcPort finds an assignable port field in one of the subprocesses. 100 | func (n *Graph) getProcPort(procName, portName string, dir reflect.ChanDir) (reflect.Value, error) { 101 | nilValue := reflect.ValueOf(nil) 102 | // Check if process exists 103 | proc, ok := n.procs[procName] 104 | if !ok { 105 | return nilValue, fmt.Errorf("getProcPort: process '%s' not found", procName) 106 | } 107 | 108 | // Check if process is settable 109 | val := reflect.ValueOf(proc) 110 | if val.Kind() == reflect.Ptr && val.IsValid() { 111 | val = val.Elem() 112 | } 113 | 114 | if !val.CanSet() { 115 | return nilValue, fmt.Errorf("getProcPort: process '%s' is not settable", procName) 116 | } 117 | 118 | // Get the port value 119 | var ( 120 | portVal reflect.Value 121 | err error 122 | ) 123 | 124 | // Check if sender is a net 125 | net, ok := val.Interface().(Graph) 126 | if ok { 127 | // Sender is a net 128 | var ports map[string]port 129 | if dir == reflect.SendDir { 130 | ports = net.outPorts 131 | } else { 132 | ports = net.inPorts 133 | } 134 | 135 | p, ok := ports[portName] 136 | if !ok { 137 | return nilValue, fmt.Errorf("getProcPort: subgraph '%s' does not have inport '%s'", procName, portName) 138 | } 139 | 140 | portVal, err = net.getProcPort(p.addr.proc, p.addr.port, dir) 141 | } else { 142 | // Sender is a proc 143 | portVal = val.FieldByName(portName) 144 | } 145 | 146 | if err == nil && (!portVal.IsValid()) { 147 | err = fmt.Errorf("process '%s' does not have a valid port '%s'", procName, portName) 148 | } 149 | 150 | if err != nil { 151 | return nilValue, fmt.Errorf("getProcPort: %w", err) 152 | } 153 | 154 | return portVal, nil 155 | } 156 | 157 | func attachPort(port reflect.Value, addr address, dir reflect.ChanDir, ch reflect.Value, bufSize int) (reflect.Value, error) { 158 | if addr.index > -1 { 159 | return attachArrayPort(port, addr.index, dir, ch, bufSize) 160 | } 161 | 162 | if addr.key != "" { 163 | return attachMapPort(port, addr.key, dir, ch, bufSize) 164 | } 165 | 166 | return attachChanPort(port, dir, ch, bufSize) 167 | } 168 | 169 | func attachChanPort(port reflect.Value, dir reflect.ChanDir, ch reflect.Value, bufSize int) (reflect.Value, error) { 170 | if err := validateChanDir(port.Type(), dir); err != nil { 171 | return ch, err 172 | } 173 | 174 | if err := validateCanSet(port); err != nil { 175 | return ch, err 176 | } 177 | 178 | ch = selectOrMakeChan(ch, port, port.Type().Elem(), bufSize) 179 | port.Set(ch) 180 | 181 | return ch, nil 182 | } 183 | 184 | func attachMapPort(port reflect.Value, key string, dir reflect.ChanDir, ch reflect.Value, bufSize int) (reflect.Value, error) { 185 | if err := validateChanDir(port.Type().Elem(), dir); err != nil { 186 | return ch, err 187 | } 188 | 189 | kv := reflect.ValueOf(key) 190 | item := port.MapIndex(kv) 191 | ch = selectOrMakeChan(ch, item, port.Type().Elem().Elem(), bufSize) 192 | 193 | if port.IsNil() { 194 | m := reflect.MakeMap(port.Type()) 195 | port.Set(m) 196 | } 197 | 198 | port.SetMapIndex(kv, ch) 199 | 200 | return ch, nil 201 | } 202 | 203 | func attachArrayPort(port reflect.Value, key int, dir reflect.ChanDir, ch reflect.Value, bufSize int) (reflect.Value, error) { 204 | if err := validateChanDir(port.Type().Elem(), dir); err != nil { 205 | return ch, err 206 | } 207 | 208 | if port.IsNil() { 209 | m := reflect.MakeSlice(port.Type(), 0, 32) 210 | port.Set(m) 211 | } 212 | 213 | if port.Cap() <= key { 214 | port.SetCap(2 * key) 215 | } 216 | 217 | if port.Len() <= key { 218 | port.SetLen(key + 1) 219 | } 220 | 221 | item := port.Index(key) 222 | ch = selectOrMakeChan(ch, item, port.Type().Elem().Elem(), bufSize) 223 | item.Set(ch) 224 | 225 | return ch, nil 226 | } 227 | 228 | func validateChanDir(portType reflect.Type, dir reflect.ChanDir) error { 229 | if portType.Kind() != reflect.Chan { 230 | return fmt.Errorf("not a channel") 231 | } 232 | 233 | if portType.ChanDir()&dir == 0 { 234 | return fmt.Errorf("channel does not support direction %s", dir.String()) 235 | } 236 | 237 | return nil 238 | } 239 | 240 | func validateCanSet(portVal reflect.Value) error { 241 | if !portVal.CanSet() { 242 | return fmt.Errorf("port is not assignable") 243 | } 244 | 245 | return nil 246 | } 247 | 248 | func selectOrMakeChan(new, existing reflect.Value, t reflect.Type, bufSize int) reflect.Value { 249 | if !new.IsValid() || new.IsNil() { 250 | if existing.IsValid() && !existing.IsNil() { 251 | return existing 252 | } 253 | 254 | chanType := reflect.ChanOf(reflect.BothDir, t) 255 | new = reflect.MakeChan(chanType, bufSize) 256 | } 257 | 258 | return new 259 | } 260 | 261 | // parseAddress unfolds a string port name into parts, including array index or hashmap key. 262 | func parseAddress(proc, port string) address { 263 | n := address{ 264 | proc: proc, 265 | port: port, 266 | index: -1, 267 | } 268 | keyPos := 0 269 | key := "" 270 | 271 | for i, r := range port { 272 | if r == '[' { 273 | keyPos = i + 1 274 | n.port = port[0:i] 275 | } 276 | 277 | if r == ']' { 278 | key = port[keyPos:i] 279 | } 280 | } 281 | 282 | n.port = capitalizePortName(n.port) 283 | 284 | if key == "" { 285 | return n 286 | } 287 | 288 | if i, err := strconv.Atoi(key); err == nil { 289 | n.index = i 290 | } else { 291 | n.key = key 292 | } 293 | 294 | n.key = key 295 | 296 | return n 297 | } 298 | 299 | // capitalizePortName converts port names defined in UPPER or lower case to Title case, 300 | // which is more common for structs in Go. 301 | func capitalizePortName(name string) string { 302 | lower := strings.ToLower(name) 303 | upper := strings.ToUpper(name) 304 | 305 | if name == lower || name == upper { 306 | return strings.Title(lower) 307 | } 308 | 309 | return name 310 | } 311 | 312 | // findExistingChan returns a channel attached to receiver if it already exists among connections. 313 | func (n *Graph) findExistingChan(addr address, dir reflect.ChanDir) reflect.Value { 314 | var channel reflect.Value 315 | // Find existing channel attached to the receiver 316 | for i := range n.connections { 317 | var a address 318 | if dir == reflect.SendDir { 319 | a = n.connections[i].src 320 | } else { 321 | a = n.connections[i].tgt 322 | } 323 | 324 | if a == addr { 325 | channel = n.connections[i].channel 326 | break 327 | } 328 | } 329 | 330 | return channel 331 | } 332 | 333 | // incChanListenersCount increments SendChanRefCount. 334 | // The count is needed when multiple senders are connected 335 | // to the same receiver. When the network is terminated and 336 | // senders need to close their output port, this counter 337 | // can help to avoid closing the same channel multiple times. 338 | func (n *Graph) incChanListenersCount(c reflect.Value) { 339 | n.chanListenersCountLock.Lock() 340 | defer n.chanListenersCountLock.Unlock() 341 | 342 | ptr := c.Pointer() 343 | cnt := n.chanListenersCount[ptr] 344 | cnt++ 345 | n.chanListenersCount[ptr] = cnt 346 | } 347 | 348 | // decChanListenersCount decrements SendChanRefCount 349 | // It returns true if the RefCount has reached 0. 350 | func (n *Graph) decChanListenersCount(c reflect.Value) bool { 351 | n.chanListenersCountLock.Lock() 352 | defer n.chanListenersCountLock.Unlock() 353 | 354 | ptr := c.Pointer() 355 | cnt := n.chanListenersCount[ptr] 356 | 357 | if cnt == 0 { 358 | return true // yes you may try to close a nonexistent channel, see what happens... 359 | } 360 | 361 | cnt-- 362 | n.chanListenersCount[ptr] = cnt 363 | 364 | return cnt == 0 365 | } 366 | 367 | // // Disconnect removes a connection between sender's outport and receiver's inport. 368 | // func (n *Graph) Disconnect(senderName, senderPort, receiverName, receiverPort string) bool { 369 | // var sender, receiver interface{} 370 | // var ok bool 371 | // sender, ok = n.procs[senderName] 372 | // if !ok { 373 | // return false 374 | // } 375 | // receiver, ok = n.procs[receiverName] 376 | // if !ok { 377 | // return false 378 | // } 379 | // res := unsetProcPort(sender, senderPort, true) 380 | // res = res && unsetProcPort(receiver, receiverPort, false) 381 | // return res 382 | // } 383 | 384 | // // Unsets an port of a given process 385 | // func unsetProcPort(proc interface{}, portName string, isOut bool) bool { 386 | // v := reflect.ValueOf(proc) 387 | // var ch reflect.Value 388 | // if v.Elem().FieldByName("Graph").IsValid() { 389 | // if subnet, ok := v.Elem().FieldByName("Graph").Addr().Interface().(*Graph); ok { 390 | // if isOut { 391 | // ch = subnet.getOutPort(portName) 392 | // } else { 393 | // ch = subnet.getInPort(portName) 394 | // } 395 | // } else { 396 | // return false 397 | // } 398 | // } else { 399 | // ch = v.Elem().FieldByName(portName) 400 | // } 401 | // if !ch.IsValid() { 402 | // return false 403 | // } 404 | // ch.Set(reflect.Zero(ch.Type())) 405 | // return true 406 | // } 407 | -------------------------------------------------------------------------------- /graph_connect_test.go: -------------------------------------------------------------------------------- 1 | package goflow 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type withInvalidPorts struct { 8 | NotChan int 9 | Chan <-chan int 10 | } 11 | 12 | func (c *withInvalidPorts) Process() { 13 | // Dummy 14 | } 15 | 16 | func TestConnectInvalidParams(t *testing.T) { 17 | n := NewGraph() 18 | 19 | n.Add("e1", new(echo)) 20 | n.Add("e2", new(echo)) 21 | n.Add("inv", new(withInvalidPorts)) 22 | 23 | cases := []struct { 24 | scenario string 25 | err error 26 | msg string 27 | }{ 28 | { 29 | "Invalid receiver proc", 30 | n.Connect("e1", "Out", "noproc", "In"), 31 | "connect: getProcPort: process 'noproc' not found", 32 | }, 33 | { 34 | "Invalid receiver port", 35 | n.Connect("e1", "Out", "e2", "NotIn"), 36 | "connect: getProcPort: process 'e2' does not have a valid port 'NotIn'", 37 | }, 38 | { 39 | "Invalid sender proc", 40 | n.Connect("noproc", "Out", "e2", "In"), 41 | "connect: getProcPort: process 'noproc' not found", 42 | }, 43 | { 44 | "Invalid sender port", 45 | n.Connect("e1", "NotOut", "e2", "In"), 46 | "connect: getProcPort: process 'e1' does not have a valid port 'NotOut'", 47 | }, 48 | { 49 | "Sending to output", 50 | n.Connect("e1", "Out", "e2", "Out"), 51 | "connect 'e2.Out': channel does not support direction <-chan", 52 | }, 53 | { 54 | "Sending from input", 55 | n.Connect("e1", "In", "e2", "In"), 56 | "connect 'e1.In': channel does not support direction chan<-", 57 | }, 58 | { 59 | "Connecting to non-chan", 60 | n.Connect("e1", "Out", "inv", "NotChan"), 61 | "connect 'inv.NotChan': not a channel", 62 | }, 63 | } 64 | 65 | for _, item := range cases { 66 | c := item 67 | t.Run(c.scenario, func(t *testing.T) { 68 | t.Parallel() 69 | if c.err == nil { 70 | t.Fail() 71 | } else if c.msg != c.err.Error() { 72 | t.Error(c.err) 73 | } 74 | }) 75 | } 76 | } 77 | 78 | func TestPortNameCapitalization(t *testing.T) { 79 | n := NewGraph() 80 | 81 | n.Add("e1", new(echo)) 82 | n.Add("e2", new(echo)) 83 | 84 | cases := []struct { 85 | scenario string 86 | err error 87 | }{ 88 | { 89 | "Capitalize lowercase port names", 90 | n.Connect("e1", "out", "e2", "in"), 91 | }, 92 | { 93 | "Capitalize uppercase port names", 94 | n.Connect("e1", "OUT", "e2", "IN"), 95 | }, 96 | } 97 | 98 | for _, item := range cases { 99 | c := item 100 | t.Run(c.scenario, func(t *testing.T) { 101 | t.Parallel() 102 | if c.err != nil { 103 | t.Error(c.err) 104 | } 105 | }) 106 | } 107 | } 108 | 109 | func TestSubgraphSender(t *testing.T) { 110 | sub, err := newDoubleEcho() 111 | if err != nil { 112 | t.Error(err) 113 | return 114 | } 115 | 116 | n := NewGraph() 117 | if err := n.Add("sub", sub); err != nil { 118 | t.Error(err) 119 | return 120 | } 121 | 122 | n.Add("e3", new(echo)) 123 | 124 | if err := n.Connect("sub", "Out", "e3", "In"); err != nil { 125 | t.Error(err) 126 | return 127 | } 128 | 129 | n.MapInPort("In", "sub", "In") 130 | n.MapOutPort("Out", "e3", "Out") 131 | 132 | testGraphWithNumberSequence(n, t) 133 | } 134 | 135 | func TestSubgraphReceiver(t *testing.T) { 136 | sub, err := newDoubleEcho() 137 | if err != nil { 138 | t.Error(err) 139 | return 140 | } 141 | 142 | n := NewGraph() 143 | if err := n.Add("sub", sub); err != nil { 144 | t.Error(err) 145 | return 146 | } 147 | 148 | n.Add("e3", new(echo)) 149 | 150 | if err := n.Connect("e3", "Out", "sub", "In"); err != nil { 151 | t.Error(err) 152 | return 153 | } 154 | 155 | n.MapInPort("In", "e3", "In") 156 | n.MapOutPort("Out", "sub", "Out") 157 | 158 | testGraphWithNumberSequence(n, t) 159 | } 160 | 161 | func newFanOutFanIn() (*Graph, error) { 162 | n := NewGraph() 163 | 164 | components := map[string]interface{}{ 165 | "e1": new(echo), 166 | "d1": new(doubler), 167 | "d2": new(doubler), 168 | "d3": new(doubler), 169 | "e2": new(echo), 170 | } 171 | 172 | for name, c := range components { 173 | if err := n.Add(name, c); err != nil { 174 | return nil, err 175 | } 176 | } 177 | 178 | connections := []struct{ sn, sp, rn, rp string }{ 179 | {"e1", "Out", "d1", "In"}, 180 | {"e1", "Out", "d2", "In"}, 181 | {"e1", "Out", "d3", "In"}, 182 | {"d1", "Out", "e2", "In"}, 183 | {"d2", "Out", "e2", "In"}, 184 | {"d3", "Out", "e2", "In"}, 185 | } 186 | 187 | for i := range connections { 188 | if err := n.Connect(connections[i].sn, connections[i].sp, connections[i].rn, connections[i].rp); err != nil { 189 | return nil, err 190 | } 191 | } 192 | 193 | n.MapInPort("In", "e1", "In") 194 | n.MapOutPort("Out", "e2", "Out") 195 | 196 | return n, nil 197 | } 198 | 199 | func TestFanOutFanIn(t *testing.T) { 200 | inData := []int{1, 2, 3, 4, 5, 6, 7, 8} 201 | outData := []int{2, 4, 6, 8, 10, 12, 14, 16} 202 | 203 | n, err := newFanOutFanIn() 204 | if err != nil { 205 | t.Error(err) 206 | return 207 | } 208 | 209 | in := make(chan int) 210 | out := make(chan int) 211 | 212 | n.SetInPort("In", in) 213 | n.SetOutPort("Out", out) 214 | 215 | wait := Run(n) 216 | 217 | go func() { 218 | for _, n := range inData { 219 | in <- n 220 | } 221 | 222 | close(in) 223 | }() 224 | 225 | i := 0 226 | 227 | for actual := range out { 228 | found := false 229 | 230 | for j := 0; j < len(outData); j++ { 231 | if outData[j] == actual { 232 | found = true 233 | 234 | outData = append(outData[:j], outData[j+1:]...) 235 | } 236 | } 237 | 238 | if !found { 239 | t.Errorf("%d not found in expected data", actual) 240 | } 241 | i++ 242 | } 243 | 244 | if i != len(inData) { 245 | t.Errorf("Output count missmatch: %d != %d", i, len(inData)) 246 | } 247 | 248 | <-wait 249 | } 250 | 251 | func newMapPorts() (*Graph, error) { 252 | n := NewGraph() 253 | 254 | components := map[string]interface{}{ 255 | "e1": new(echo), 256 | "e11": new(echo), 257 | "e22": new(echo), 258 | "r": new(router), 259 | } 260 | 261 | for name, c := range components { 262 | if err := n.Add(name, c); err != nil { 263 | return nil, err 264 | } 265 | } 266 | 267 | connections := []struct{ sn, sp, rn, rp string }{ 268 | {"e1", "Out", "r", "In[e1]"}, 269 | {"r", "Out[e2]", "e22", "In"}, 270 | {"r", "Out[e1]", "e11", "In"}, 271 | } 272 | 273 | for i := range connections { 274 | if err := n.Connect(connections[i].sn, connections[i].sp, connections[i].rn, connections[i].rp); err != nil { 275 | return nil, err 276 | } 277 | } 278 | 279 | iips := []struct { 280 | proc, port string 281 | v int 282 | }{ 283 | {"e1", "In", 1}, 284 | {"r", "In[e3]", 3}, 285 | } 286 | 287 | for i := range iips { 288 | if err := n.AddIIP(iips[i].proc, iips[i].port, iips[i].v); err != nil { 289 | return nil, err 290 | } 291 | } 292 | 293 | n.MapInPort("I2", "r", "In[e2]") 294 | 295 | outPorts := []struct{ pn, pp, name string }{ 296 | {"e11", "Out", "O1"}, 297 | {"e22", "Out", "O2"}, 298 | {"r", "Out[e3]", "O3"}, 299 | } 300 | 301 | for i := range outPorts { 302 | n.MapOutPort(outPorts[i].name, outPorts[i].pn, outPorts[i].pp) 303 | } 304 | 305 | return n, nil 306 | } 307 | 308 | func TestMapPorts(t *testing.T) { 309 | n, err := newMapPorts() 310 | if err != nil { 311 | t.Error(err) 312 | return 313 | } 314 | 315 | i2 := make(chan int, 1) 316 | o1 := make(chan int) 317 | o2 := make(chan int) 318 | o3 := make(chan int) 319 | 320 | if err := n.SetInPort("I2", i2); err != nil { 321 | t.Error(err) 322 | return 323 | } 324 | 325 | n.SetOutPort("O1", o1) 326 | n.SetOutPort("O2", o2) 327 | 328 | if err := n.SetOutPort("O3", o3); err != nil { 329 | t.Error(err) 330 | return 331 | } 332 | 333 | wait := Run(n) 334 | 335 | i2 <- 2 336 | close(i2) 337 | 338 | v1 := <-o1 339 | v2 := <-o2 340 | v3 := <-o3 341 | 342 | expected := []int{1, 2, 3} 343 | actual := []int{v1, v2, v3} 344 | 345 | for i, v := range actual { 346 | if v != expected[i] { 347 | t.Errorf("Expected %d, got %d", expected[i], v) 348 | } 349 | } 350 | 351 | <-wait 352 | } 353 | 354 | func newArrayPorts() (*Graph, error) { 355 | n := NewGraph() 356 | 357 | components := map[string]interface{}{ 358 | "e0": new(echo), 359 | "e00": new(echo), 360 | "e11": new(echo), 361 | "r": new(irouter), 362 | } 363 | 364 | for name, c := range components { 365 | if err := n.Add(name, c); err != nil { 366 | return nil, err 367 | } 368 | } 369 | 370 | connections := []struct{ sn, sp, rn, rp string }{ 371 | {"e0", "Out", "r", "In[0]"}, 372 | {"r", "Out[1]", "e11", "In"}, 373 | {"r", "Out[0]", "e00", "In"}, 374 | } 375 | 376 | for i := range connections { 377 | if err := n.Connect(connections[i].sn, connections[i].sp, connections[i].rn, connections[i].rp); err != nil { 378 | return nil, err 379 | } 380 | } 381 | 382 | iips := []struct { 383 | proc, port string 384 | v int 385 | }{ 386 | {"e0", "In", 1}, 387 | {"r", "In[2]", 3}, 388 | } 389 | 390 | for i := range iips { 391 | if err := n.AddIIP(iips[i].proc, iips[i].port, iips[i].v); err != nil { 392 | return nil, err 393 | } 394 | } 395 | 396 | n.MapInPort("I1", "r", "In[1]") 397 | 398 | outPorts := []struct{ pn, pp, name string }{ 399 | {"e00", "Out", "O0"}, 400 | {"e11", "Out", "O1"}, 401 | {"r", "Out[2]", "O2"}, 402 | } 403 | 404 | for i := range outPorts { 405 | n.MapOutPort(outPorts[i].name, outPorts[i].pn, outPorts[i].pp) 406 | } 407 | 408 | return n, nil 409 | } 410 | 411 | func TestArrayPorts(t *testing.T) { 412 | n, err := newArrayPorts() 413 | if err != nil { 414 | t.Error(err) 415 | return 416 | } 417 | 418 | i1 := make(chan int, 1) 419 | o0 := make(chan int) 420 | o1 := make(chan int) 421 | o2 := make(chan int) 422 | 423 | if err := n.SetInPort("I1", i1); err != nil { 424 | t.Error(err) 425 | return 426 | } 427 | 428 | n.SetOutPort("O0", o0) 429 | n.SetOutPort("O1", o1) 430 | 431 | if err := n.SetOutPort("O2", o2); err != nil { 432 | t.Error(err) 433 | return 434 | } 435 | 436 | wait := Run(n) 437 | 438 | i1 <- 2 439 | close(i1) 440 | 441 | v0 := <-o0 442 | v1 := <-o1 443 | v2 := <-o2 444 | 445 | expected := []int{1, 2, 3} 446 | actual := []int{v0, v1, v2} 447 | 448 | for i, v := range actual { 449 | if v != expected[i] { 450 | t.Errorf("Expected %d, got %d", expected[i], v) 451 | } 452 | } 453 | 454 | <-wait 455 | } 456 | -------------------------------------------------------------------------------- /graph_iip.go: -------------------------------------------------------------------------------- 1 | package goflow 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | // iip is the Initial Information Packet. 9 | // IIPs are delivered to process input ports on the network start. 10 | type iip struct { 11 | data interface{} 12 | addr address 13 | } 14 | 15 | // AddIIP adds an Initial Information packet to the network. 16 | func (n *Graph) AddIIP(processName, portName string, data interface{}) error { 17 | addr := parseAddress(processName, portName) 18 | 19 | if _, exists := n.procs[processName]; exists { 20 | n.iips = append(n.iips, iip{data: data, addr: addr}) 21 | return nil 22 | } 23 | 24 | return fmt.Errorf("AddIIP: could not find '%s'", addr) 25 | } 26 | 27 | // RemoveIIP detaches an IIP from specific process and port. 28 | func (n *Graph) RemoveIIP(processName, portName string) error { 29 | addr := parseAddress(processName, portName) 30 | for i := range n.iips { 31 | if n.iips[i].addr == addr { 32 | // Remove item from the slice 33 | n.iips[len(n.iips)-1], n.iips[i], n.iips = iip{}, n.iips[len(n.iips)-1], n.iips[:len(n.iips)-1] 34 | return nil 35 | } 36 | } 37 | 38 | return fmt.Errorf("RemoveIIP: could not find IIP for '%s'", addr) 39 | } 40 | 41 | // sendIIPs sends Initial Information Packets upon network start. 42 | func (n *Graph) sendIIPs() error { 43 | // Send initial IPs 44 | for i := range n.iips { 45 | ip := n.iips[i] 46 | 47 | // Get the receiver port channel 48 | channel, found := n.channelByInPortAddr(ip.addr) 49 | 50 | if !found { 51 | channel, found = n.channelByConnectionAddr(ip.addr) 52 | } 53 | 54 | if !found { 55 | // Try to find a proc and attach a new channel to it 56 | recvPort, err := n.getProcPort(ip.addr.proc, ip.addr.port, reflect.RecvDir) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | channel, err = attachPort(recvPort, ip.addr, reflect.RecvDir, reflect.ValueOf(nil), n.conf.BufferSize) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | found = true 67 | } 68 | 69 | if !found { 70 | return fmt.Errorf("IIP target not found: '%s'", ip.addr) 71 | } 72 | 73 | // Increase reference count for the channel 74 | n.incChanListenersCount(channel) 75 | 76 | // Send data to the port 77 | go func(channel, data reflect.Value) { 78 | channel.Send(data) 79 | 80 | if n.decChanListenersCount(channel) { 81 | channel.Close() 82 | } 83 | }(channel, reflect.ValueOf(ip.data)) 84 | } 85 | 86 | return nil 87 | } 88 | 89 | // channelByInPortAddr returns a channel by address from the network inports. 90 | func (n *Graph) channelByInPortAddr(addr address) (channel reflect.Value, found bool) { 91 | for i := range n.inPorts { 92 | if n.inPorts[i].addr == addr { 93 | return n.inPorts[i].channel, true 94 | } 95 | } 96 | 97 | return reflect.Value{}, false 98 | } 99 | 100 | // channelByConnectionAddr returns a channel by address from connections. 101 | func (n *Graph) channelByConnectionAddr(addr address) (channel reflect.Value, found bool) { 102 | for i := range n.connections { 103 | if n.connections[i].tgt == addr { 104 | return n.connections[i].channel, true 105 | } 106 | } 107 | 108 | return reflect.Value{}, false 109 | } 110 | -------------------------------------------------------------------------------- /graph_iip_test.go: -------------------------------------------------------------------------------- 1 | package goflow 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func newRepeatGraph() (*Graph, error) { 9 | n := NewGraph() 10 | 11 | if err := n.Add("r", new(repeater)); err != nil { 12 | return nil, err 13 | } 14 | 15 | n.MapInPort("Word", "r", "Word") 16 | n.MapOutPort("Words", "r", "Words") 17 | 18 | return n, nil 19 | } 20 | 21 | func TestBasicIIP(t *testing.T) { 22 | qty := 5 23 | 24 | n, err := newRepeatGraph() 25 | if err != nil { 26 | t.Error(err) 27 | return 28 | } 29 | 30 | if err := n.AddIIP("r", "Times", qty); err != nil { 31 | t.Error(err) 32 | return 33 | } 34 | 35 | input := "hello" 36 | output := []string{"hello", "hello", "hello", "hello", "hello"} 37 | 38 | in := make(chan string) 39 | out := make(chan string) 40 | 41 | if err := n.SetInPort("Word", in); err != nil { 42 | t.Error(err) 43 | return 44 | } 45 | 46 | if err := n.SetOutPort("Words", out); err != nil { 47 | t.Error(err) 48 | return 49 | } 50 | 51 | wait := Run(n) 52 | 53 | go func() { 54 | in <- input 55 | close(in) 56 | }() 57 | 58 | i := 0 59 | 60 | for actual := range out { 61 | expected := output[i] 62 | if actual != expected { 63 | t.Errorf("%s != %s", actual, expected) 64 | } 65 | i++ 66 | } 67 | 68 | if i != qty { 69 | t.Errorf("Returned %d words instead of %d", i, qty) 70 | } 71 | 72 | <-wait 73 | } 74 | 75 | func newRepeatGraph2Ins() (*Graph, error) { 76 | n := NewGraph() 77 | 78 | if err := n.Add("r", new(repeater)); err != nil { 79 | return nil, err 80 | } 81 | 82 | n.MapInPort("Word", "r", "Word") 83 | n.MapInPort("Times", "r", "Times") 84 | n.MapOutPort("Words", "r", "Words") 85 | 86 | return n, nil 87 | } 88 | 89 | func TestGraphInportIIP(t *testing.T) { //nolint:funlen // long setup 90 | n, err := newRepeatGraph2Ins() 91 | if err != nil { 92 | t.Error(err) 93 | return 94 | } 95 | 96 | input := "hello" 97 | output := []string{"hello", "hello", "hello", "hello", "hello"} 98 | qty := len(output) 99 | 100 | in := make(chan string) 101 | times := make(chan int) 102 | out := make(chan string) 103 | 104 | if err := n.SetInPort("Word", in); err != nil { 105 | t.Error(err) 106 | return 107 | } 108 | 109 | if err := n.SetInPort("Times", times); err != nil { 110 | t.Error(err) 111 | return 112 | } 113 | 114 | if err := n.SetOutPort("Words", out); err != nil { 115 | t.Error(err) 116 | return 117 | } 118 | 119 | if err := n.AddIIP("r", "Times", qty); err != nil { 120 | t.Error(err) 121 | return 122 | } 123 | 124 | wait := Run(n) 125 | 126 | go func() { 127 | in <- input 128 | close(in) 129 | }() 130 | 131 | // As times channel is referenced from both IIP and external connection, 132 | // it needs reference counting to avoid data race when closing 133 | vTimes := reflect.ValueOf(times) 134 | n.incChanListenersCount(vTimes) 135 | 136 | i := 0 137 | for actual := range out { 138 | if i == 0 && n.decChanListenersCount(vTimes) { 139 | // The graph inport needs to be closed once the IIP is sent 140 | close(times) 141 | } 142 | 143 | if expected := output[i]; actual != expected { 144 | t.Errorf("%s != %s", actual, expected) 145 | } 146 | i++ 147 | } 148 | 149 | if i != qty { 150 | t.Errorf("Returned %d words instead of %d", i, qty) 151 | } 152 | 153 | <-wait 154 | } 155 | 156 | func TestInternalConnectionIIP(t *testing.T) { 157 | input := 1 158 | iip := 2 159 | output := []int{1, 2} 160 | qty := 2 161 | 162 | n, err := newDoubleEcho() 163 | if err != nil { 164 | t.Error(err) 165 | return 166 | } 167 | 168 | if err := n.AddIIP("e2", "In", iip); err != nil { 169 | t.Error(err) 170 | return 171 | } 172 | 173 | in := make(chan int) 174 | out := make(chan int) 175 | 176 | if err := n.SetInPort("In", in); err != nil { 177 | t.Error(err) 178 | return 179 | } 180 | 181 | if err := n.SetOutPort("Out", out); err != nil { 182 | t.Error(err) 183 | return 184 | } 185 | 186 | wait := Run(n) 187 | 188 | go func() { 189 | in <- input 190 | close(in) 191 | }() 192 | 193 | i := 0 194 | 195 | for actual := range out { 196 | // The order of output is not guaranteed in this case 197 | if actual != output[0] && actual != output[1] { 198 | t.Errorf("Unexpected value %d", actual) 199 | } 200 | i++ 201 | } 202 | 203 | if i != qty { 204 | t.Errorf("Returned %d words instead of %d", i, qty) 205 | } 206 | 207 | <-wait 208 | } 209 | 210 | func TestAddRemoveIIP(t *testing.T) { 211 | n := NewGraph() 212 | 213 | if err := n.Add("e", new(echo)); err != nil { 214 | t.Error(err) 215 | return 216 | } 217 | 218 | if err := n.AddIIP("e", "In", 5); err != nil { 219 | t.Error(err) 220 | return 221 | } 222 | 223 | // Adding an IIP to a non-existing process/port should fail 224 | if err := n.AddIIP("d", "No", 404); err == nil { 225 | t.FailNow() 226 | return 227 | } 228 | 229 | if err := n.RemoveIIP("e", "In"); err != nil { 230 | t.Error(err) 231 | return 232 | } 233 | 234 | // Second attempt to remove same IIP should fail 235 | if err := n.RemoveIIP("e", "In"); err == nil { 236 | t.FailNow() 237 | return 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /graph_ports.go: -------------------------------------------------------------------------------- 1 | package goflow 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | // port within the network. 9 | type port struct { 10 | addr address // Address of the port in the graph 11 | channel reflect.Value // Actual channel attached 12 | info PortInfo // Runtime info 13 | } 14 | 15 | // MapInPort adds an inport to the net and maps it to a contained proc's port. 16 | func (n *Graph) MapInPort(name, procName, procPort string) { 17 | addr := parseAddress(procName, procPort) 18 | n.inPorts[name] = port{addr: addr} 19 | } 20 | 21 | // // AnnotateInPort sets optional run-time annotation for the port utilized by 22 | // // runtimes and FBP protocol clients. 23 | // func (n *Graph) AnnotateInPort(name string, info PortInfo) bool { 24 | // port, exists := n.inPorts[name] 25 | // if !exists { 26 | // return false 27 | // } 28 | // port.info = info 29 | // return true 30 | // } 31 | 32 | // // UnmapInPort removes an existing inport mapping 33 | // func (n *Graph) UnmapInPort(name string) bool { 34 | // if _, exists := n.inPorts[name]; !exists { 35 | // return false 36 | // } 37 | // delete(n.inPorts, name) 38 | // return true 39 | // } 40 | 41 | // MapOutPort adds an outport to the net and maps it to a contained proc's port. 42 | func (n *Graph) MapOutPort(name, procName, procPort string) { 43 | addr := parseAddress(procName, procPort) 44 | n.outPorts[name] = port{addr: addr} 45 | } 46 | 47 | // // AnnotateOutPort sets optional run-time annotation for the port utilized by 48 | // // runtimes and FBP protocol clients. 49 | // func (n *Graph) AnnotateOutPort(name string, info PortInfo) bool { 50 | // port, exists := n.outPorts[name] 51 | // if !exists { 52 | // return false 53 | // } 54 | // port.info = info 55 | // return true 56 | // } 57 | 58 | // // UnmapOutPort removes an existing outport mapping 59 | // func (n *Graph) UnmapOutPort(name string) bool { 60 | // if _, exists := n.outPorts[name]; !exists { 61 | // return false 62 | // } 63 | // delete(n.outPorts, name) 64 | // return true 65 | // } 66 | 67 | // SetInPort assigns a channel to a network's inport to talk to the outer world. 68 | func (n *Graph) SetInPort(name string, channel interface{}) error { 69 | return n.setGraphPort(name, channel, reflect.RecvDir) 70 | } 71 | 72 | // SetOutPort assigns a channel to a network's outport to talk to the outer world. 73 | // It returns true on success or false if the outport cannot be set. 74 | func (n *Graph) SetOutPort(name string, channel interface{}) error { 75 | return n.setGraphPort(name, channel, reflect.SendDir) 76 | } 77 | 78 | func (n *Graph) setGraphPort(name string, channel interface{}, dir reflect.ChanDir) error { 79 | var ( 80 | ports map[string]port 81 | dirDescr string 82 | ) 83 | 84 | if dir == reflect.SendDir { 85 | ports = n.outPorts 86 | dirDescr = "out" 87 | } else { 88 | ports = n.inPorts 89 | dirDescr = "in" 90 | } 91 | 92 | p, ok := ports[name] 93 | if !ok { 94 | return fmt.Errorf("setGraphPort: %s port '%s' not defined", dirDescr, name) 95 | } 96 | 97 | // Try to attach it 98 | port, err := n.getProcPort(p.addr.proc, p.addr.port, dir) 99 | if err != nil { 100 | return fmt.Errorf("setGraphPort: cannot set %s port '%s': %w", dirDescr, name, err) 101 | } 102 | 103 | if _, err = attachPort(port, p.addr, dir, reflect.ValueOf(channel), n.conf.BufferSize); err != nil { 104 | return fmt.Errorf("setGraphPort: cannot attach %s port '%s': %w", dirDescr, name, err) 105 | } 106 | 107 | // Save it in inPorts to be used with IIPs if needed 108 | p.channel = reflect.ValueOf(channel) 109 | ports[name] = p 110 | 111 | return nil 112 | } 113 | 114 | // // RenameInPort changes graph's inport name 115 | // func (n *Graph) RenameInPort(oldName, newName string) bool { 116 | // if _, exists := n.inPorts[oldName]; !exists { 117 | // return false 118 | // } 119 | // n.inPorts[newName] = n.inPorts[oldName] 120 | // delete(n.inPorts, oldName) 121 | // return true 122 | // } 123 | 124 | // // UnsetInPort removes an external inport from the graph 125 | // func (n *Graph) UnsetInPort(name string) bool { 126 | // port, exists := n.inPorts[name] 127 | // if !exists { 128 | // return false 129 | // } 130 | // if proc, ok := n.procs[port.proc]; ok { 131 | // unsetProcPort(proc, port.port, false) 132 | // } 133 | // delete(n.inPorts, name) 134 | // return true 135 | // } 136 | 137 | // // RenameOutPort changes graph's outport name 138 | // func (n *Graph) RenameOutPort(oldName, newName string) bool { 139 | // if _, exists := n.outPorts[oldName]; !exists { 140 | // return false 141 | // } 142 | // n.outPorts[newName] = n.outPorts[oldName] 143 | // delete(n.outPorts, oldName) 144 | // return true 145 | // } 146 | 147 | // // UnsetOutPort removes an external outport from the graph 148 | // func (n *Graph) UnsetOutPort(name string) bool { 149 | // port, exists := n.outPorts[name] 150 | // if !exists { 151 | // return false 152 | // } 153 | // if proc, ok := n.procs[port.proc]; ok { 154 | // unsetProcPort(proc, port.proc, true) 155 | // } 156 | // delete(n.outPorts, name) 157 | // return true 158 | // } 159 | -------------------------------------------------------------------------------- /graph_ports_test.go: -------------------------------------------------------------------------------- 1 | package goflow 2 | 3 | import "testing" 4 | 5 | func TestOutportNotFound(t *testing.T) { 6 | sub, err := newDoubleEcho() 7 | if err != nil { 8 | t.Error(err) 9 | return 10 | } 11 | 12 | n := NewGraph() 13 | if err := n.Add("sub", sub); err != nil { 14 | t.Error(err) 15 | return 16 | } 17 | 18 | n.Add("e3", new(echo)) 19 | 20 | if err := n.Connect("sub", "NoOut", "e3", "In"); err == nil { 21 | t.Errorf("Expected an error") 22 | } 23 | } 24 | 25 | func TestInPortNotFound(t *testing.T) { 26 | sub, err := newDoubleEcho() 27 | if err != nil { 28 | t.Error(err) 29 | return 30 | } 31 | 32 | n := NewGraph() 33 | if err := n.Add("sub", sub); err != nil { 34 | t.Error(err) 35 | return 36 | } 37 | 38 | n.Add("e3", new(echo)) 39 | 40 | if err := n.Connect("e3", "Out", "sub", "NotIn"); err == nil { 41 | t.Errorf("Expected an error") 42 | } 43 | } 44 | 45 | func TestSetMissingProcPorts(t *testing.T) { 46 | n := NewGraph() 47 | 48 | if err := n.Add("e1", new(echo)); err != nil { 49 | t.Error(err) 50 | return 51 | } 52 | 53 | n.MapInPort("In", "nope", "In") 54 | n.MapOutPort("Out", "nope", "Out") 55 | 56 | if err := n.SetInPort("In", make(chan int)); err == nil { 57 | t.Errorf("Expected an error") 58 | return 59 | } 60 | 61 | if err := n.SetOutPort("Out", make(chan int)); err == nil { 62 | t.Errorf("Expected an error") 63 | return 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /graph_test.go: -------------------------------------------------------------------------------- 1 | package goflow 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func newDoubleEcho() (*Graph, error) { 8 | n := NewGraph() 9 | // Components 10 | e1 := new(echo) 11 | e2 := new(echo) 12 | 13 | // Structure 14 | if err := n.Add("e1", e1); err != nil { 15 | return nil, err 16 | } 17 | 18 | if err := n.Add("e2", e2); err != nil { 19 | return nil, err 20 | } 21 | 22 | if err := n.Connect("e1", "Out", "e2", "In"); err != nil { 23 | return nil, err 24 | } 25 | 26 | // Ports 27 | n.MapInPort("In", "e1", "In") 28 | n.MapOutPort("Out", "e2", "Out") 29 | 30 | return n, nil 31 | } 32 | 33 | func TestSimpleGraph(t *testing.T) { 34 | n, err := newDoubleEcho() 35 | if err != nil { 36 | t.Error(err) 37 | return 38 | } 39 | 40 | testGraphWithNumberSequence(n, t) 41 | } 42 | 43 | func testGraphWithNumberSequence(n *Graph, t *testing.T) { 44 | data := []int{7, 97, 16, 356, 81} 45 | 46 | in := make(chan int) 47 | out := make(chan int) 48 | 49 | n.SetInPort("In", in) 50 | n.SetOutPort("Out", out) 51 | 52 | wait := Run(n) 53 | 54 | go func() { 55 | for _, n := range data { 56 | in <- n 57 | } 58 | 59 | close(in) 60 | }() 61 | 62 | i := 0 63 | 64 | for actual := range out { 65 | expected := data[i] 66 | if actual != expected { 67 | t.Errorf("%d != %d", actual, expected) 68 | } 69 | i++ 70 | } 71 | 72 | <-wait 73 | } 74 | 75 | func TestAddInvalidProcess(t *testing.T) { 76 | s := struct{ Name string }{"This is not a Component"} 77 | n := NewGraph() 78 | err := n.Add("wrong", s) 79 | 80 | if err == nil { 81 | t.Errorf("Expected an error") 82 | } 83 | } 84 | 85 | func TestRemove(t *testing.T) { 86 | n := NewGraph() 87 | e1 := new(echo) 88 | 89 | if err := n.Add("e1", e1); err != nil { 90 | t.Error(err) 91 | return 92 | } 93 | 94 | if err := n.Remove("e1"); err != nil { 95 | t.Error(err) 96 | return 97 | } 98 | 99 | if err := n.Remove("e2"); err == nil { 100 | t.Errorf("Expected an error") 101 | return 102 | } 103 | } 104 | 105 | func RegisterTestGraph(f *Factory) error { 106 | f.Register("doubleEcho", func() (interface{}, error) { 107 | return newDoubleEcho() 108 | }) 109 | 110 | f.Annotate("doubleEcho", Annotation{ 111 | Description: "Contains a chain of two echo components", 112 | }) 113 | 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /loader.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package goflow 4 | 5 | import ( 6 | "encoding/json" 7 | "io/ioutil" 8 | "reflect" 9 | "strings" 10 | ) 11 | 12 | // Internal representation of NoFlo JSON format 13 | type graphDescription struct { 14 | Properties struct { 15 | Name string 16 | } 17 | Processes map[string]struct { 18 | Component string 19 | Metadata struct { 20 | Sync bool `json:",omitempty"` 21 | PoolSize int64 `json:",omitempty"` 22 | } `json:",omitempty"` 23 | } 24 | Connections []struct { 25 | Data interface{} `json:",omitempty"` 26 | Src struct { 27 | Process string 28 | Port string 29 | } `json:",omitempty"` 30 | Tgt struct { 31 | Process string 32 | Port string 33 | } 34 | Metadata struct { 35 | Buffer int `json:",omitempty"` 36 | } `json:",omitempty"` 37 | } 38 | Exports []struct { 39 | Private string 40 | Public string 41 | } 42 | } 43 | 44 | // ParseJSON converts a JSON network definition string into 45 | // a flow.Graph object that can be run or used in other networks 46 | func ParseJSON(js []byte) *Graph { 47 | // Parse JSON into Go struct 48 | var descr graphDescription 49 | err := json.Unmarshal(js, &descr) 50 | if err != nil { 51 | return nil 52 | } 53 | // fmt.Printf("%+v\n", descr) 54 | 55 | constructor := func() interface{} { 56 | // Create a new Graph 57 | net := new(Graph) 58 | net.InitGraphState() 59 | 60 | // Add processes to the network 61 | for procName, procValue := range descr.Processes { 62 | net.AddNew(procValue.Component, procName) 63 | // Process mode detection 64 | if procValue.Metadata.PoolSize > 0 { 65 | proc := net.Get(procName).(*Component) 66 | proc.Mode = ComponentModePool 67 | proc.PoolSize = uint8(procValue.Metadata.PoolSize) 68 | } else if procValue.Metadata.Sync { 69 | proc := net.Get(procName).(*Component) 70 | proc.Mode = ComponentModeSync 71 | } 72 | } 73 | 74 | // Add connections 75 | for _, conn := range descr.Connections { 76 | // Check if it is an IIP or actual connection 77 | if conn.Data == nil { 78 | // Add a connection 79 | net.ConnectBuf(conn.Src.Process, conn.Src.Port, conn.Tgt.Process, conn.Tgt.Port, conn.Metadata.Buffer) 80 | } else { 81 | // Add an IIP 82 | net.AddIIP(conn.Data, conn.Tgt.Process, conn.Tgt.Port) 83 | } 84 | } 85 | 86 | // Add port exports 87 | for _, export := range descr.Exports { 88 | // Split private into proc.port 89 | procName := export.Private[:strings.Index(export.Private, ".")] 90 | procPort := export.Private[strings.Index(export.Private, ".")+1:] 91 | // Try to detect port direction using reflection 92 | procType := reflect.TypeOf(net.Get(procName)).Elem() 93 | field, fieldFound := procType.FieldByName(procPort) 94 | if !fieldFound { 95 | panic("Private port '" + export.Private + "' not found") 96 | } 97 | if field.Type.Kind() == reflect.Chan && (field.Type.ChanDir()&reflect.RecvDir) != 0 { 98 | // It's an inport 99 | net.MapInPort(export.Public, procName, procPort) 100 | } else if field.Type.Kind() == reflect.Chan && (field.Type.ChanDir()&reflect.SendDir) != 0 { 101 | // It's an outport 102 | net.MapOutPort(export.Public, procName, procPort) 103 | } else { 104 | // It's not a proper port 105 | panic("Private port '" + export.Private + "' is not a valid channel") 106 | } 107 | // TODO add support for subgraphs 108 | } 109 | 110 | return net 111 | } 112 | 113 | // Register a component to be reused 114 | if descr.Properties.Name != "" { 115 | Register(descr.Properties.Name, constructor) 116 | } 117 | 118 | return constructor().(*Graph) 119 | } 120 | 121 | // LoadJSON loads a JSON graph definition file into 122 | // a flow.Graph object that can be run or used in other networks 123 | func LoadJSON(filename string) *Graph { 124 | js, err := ioutil.ReadFile(filename) 125 | if err != nil { 126 | return nil 127 | } 128 | return ParseJSON(js) 129 | } 130 | 131 | // RegisterJSON registers an external JSON graph definition as a component 132 | // that can be instantiated at run-time using component Factory. 133 | // It returns true on success or false if component name is already taken. 134 | func RegisterJSON(componentName, filePath string) bool { 135 | var constructor ComponentConstructor 136 | constructor = func() interface{} { 137 | return LoadJSON(filePath) 138 | } 139 | return Register(componentName, constructor) 140 | } 141 | -------------------------------------------------------------------------------- /loader_test.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package goflow 4 | 5 | import ( 6 | "sync" 7 | "testing" 8 | ) 9 | 10 | // Starter component fires the network 11 | // by sending a number given in its constructor 12 | // to its output port. 13 | type starter struct { 14 | Component 15 | Start <-chan float64 16 | Out chan<- int 17 | } 18 | 19 | func (s *starter) OnStart(num float64) { 20 | s.Out <- int(num) 21 | } 22 | 23 | func newStarter() interface{} { 24 | s := new(starter) 25 | return s 26 | } 27 | 28 | func init() { 29 | Register("starter", newStarter) 30 | } 31 | 32 | // SequenceGenerator generates a sequence of integers 33 | // from 0 to a number passed to its input. 34 | type sequenceGenerator struct { 35 | Component 36 | Num <-chan int 37 | Sequence chan<- int 38 | } 39 | 40 | func (s *sequenceGenerator) OnNum(n int) { 41 | for i := 1; i <= n; i++ { 42 | s.Sequence <- i 43 | } 44 | } 45 | 46 | func newSequenceGenerator() interface{} { 47 | return new(sequenceGenerator) 48 | } 49 | 50 | func init() { 51 | Register("sequenceGenerator", newSequenceGenerator) 52 | } 53 | 54 | // Summarizer component sums all its input packets and 55 | // produces a sum output just before shutdown 56 | type summarizer struct { 57 | Component 58 | In <-chan int 59 | // Flush <-chan bool 60 | Sum chan<- int 61 | StateLock *sync.Mutex 62 | 63 | current int 64 | } 65 | 66 | func newSummarizer() interface{} { 67 | s := new(summarizer) 68 | s.Component.Mode = ComponentModeSync 69 | return s 70 | } 71 | 72 | func init() { 73 | Register("summarizer", newSummarizer) 74 | } 75 | 76 | func (s *summarizer) OnIn(i int) { 77 | s.current += i 78 | } 79 | 80 | func (s *summarizer) Finish() { 81 | s.Sum <- s.current 82 | } 83 | 84 | var runtimeNetworkJSON = `{ 85 | "properties": { 86 | "name": "runtimeNetwork" 87 | }, 88 | "processes": { 89 | "starter": { 90 | "component": "starter" 91 | }, 92 | "generator": { 93 | "component": "sequenceGenerator" 94 | }, 95 | "doubler": { 96 | "component": "doubler" 97 | }, 98 | "sum": { 99 | "component": "summarizer" 100 | } 101 | }, 102 | "connections": [ 103 | { 104 | "data": 10, 105 | "tgt": { 106 | "process": "starter", 107 | "port": "Start" 108 | } 109 | }, 110 | { 111 | "src": { 112 | "process": "starter", 113 | "port": "Out" 114 | }, 115 | "tgt": { 116 | "process": "generator", 117 | "port": "Num" 118 | } 119 | }, 120 | { 121 | "src": { 122 | "process": "generator", 123 | "port": "Sequence" 124 | }, 125 | "tgt": { 126 | "process": "doubler", 127 | "port": "In" 128 | } 129 | }, 130 | { 131 | "src": { 132 | "process": "doubler", 133 | "port": "Out" 134 | }, 135 | "tgt": { 136 | "process": "sum", 137 | "port": "In" 138 | } 139 | } 140 | ], 141 | "exports": [ 142 | { 143 | "private": "starter.Start", 144 | "public": "Start" 145 | }, 146 | { 147 | "private": "sum.Sum", 148 | "public": "Out" 149 | } 150 | ] 151 | }` 152 | 153 | func TestRuntimeNetwork(t *testing.T) { 154 | net := ParseJSON([]byte(runtimeNetworkJSON)) 155 | if net == nil { 156 | t.Error("Could not load JSON") 157 | } 158 | 159 | start := make(chan float64) 160 | out := make(chan int) 161 | 162 | net.SetInPort("Start", start) 163 | net.SetOutPort("Out", out) 164 | 165 | RunNet(net) 166 | 167 | // Wait for the network setup 168 | <-net.Ready() 169 | 170 | // Close start to halt it normally 171 | close(start) 172 | 173 | i := <-out 174 | if i != 110 { 175 | t.Errorf("Wrong result: %d != 110", i) 176 | } 177 | 178 | <-net.Wait() 179 | } 180 | -------------------------------------------------------------------------------- /protocol.go: -------------------------------------------------------------------------------- 1 | package goflow 2 | 3 | // PortInfo represents a port to a runtime client. 4 | type PortInfo struct { 5 | ID string `json:"id"` 6 | Type string `json:"type"` 7 | Description string `json:"description"` 8 | Addressable bool `json:"addressable"` // ignored 9 | Required bool `json:"required"` 10 | Values []interface{} `json:"values"` // ignored 11 | Default interface{} `json:"default"` // ignored 12 | } 13 | 14 | // ComponentInfo represents a component to a protocol client. 15 | type ComponentInfo struct { 16 | Name string `json:"name"` 17 | Description string `json:"description"` 18 | Icon string `json:"icon"` 19 | Subgraph bool `json:"subgraph"` 20 | InPorts []PortInfo `json:"inPorts"` 21 | OutPorts []PortInfo `json:"outPorts"` 22 | } 23 | 24 | // Message represents a single FBP protocol message. 25 | type Message struct { 26 | // Protocol is NoFlo protocol identifier: 27 | // "runtime", "component", "graph" or "network" 28 | Protocol string `json:"protocol"` 29 | // Command is a command to be executed within the protocol 30 | Command string `json:"command"` 31 | // Payload is JSON-encoded body of the message 32 | Payload interface{} `json:"payload"` 33 | } 34 | 35 | // runtimeInfo message contains response to runtime.getruntime request. 36 | type runtimeInfo struct { 37 | Type string `json:"type"` 38 | Version string `json:"version"` 39 | Capabilities []string `json:"capabilities"` 40 | ID string `json:"id"` 41 | } 42 | 43 | type runtimeMessage struct { 44 | Protocol string `json:"protocol"` 45 | Command string `json:"command"` 46 | Payload runtimeInfo `json:"payload"` 47 | } 48 | 49 | // clearGraph message is sent by client to create a new empty graph. 50 | type clearGraph struct { 51 | ID string 52 | Name string `json:",omitempty"` // ignored 53 | Library string `json:",omitempty"` // ignored 54 | Main bool `json:",omitempty"` 55 | Icon string `json:",omitempty"` 56 | Description string `json:",omitempty"` 57 | } 58 | 59 | // addNode message is sent by client to add a node to a graph. 60 | type addNode struct { 61 | ID string 62 | Component string 63 | Graph string 64 | Metadata map[string]interface{} `json:",omitempty"` // ignored 65 | } 66 | 67 | // removeNode is a client message to remove a node from a graph. 68 | type removeNode struct { 69 | ID string 70 | Graph string 71 | } 72 | 73 | // renameNode is a client message to rename a node in a graph. 74 | type renameNode struct { 75 | From string 76 | To string 77 | Graph string 78 | } 79 | 80 | // changeNode is a client message to change the metadata associated with a node in the graph. 81 | type changeNode struct { // ignored 82 | ID string 83 | Graph string 84 | Metadata map[string]interface{} 85 | } 86 | 87 | // addEdge is a client message to create a connection in a graph. 88 | type addEdge struct { 89 | Src struct { 90 | Node string 91 | Port string 92 | Index int `json:",omitempty"` // ignored 93 | } 94 | Tgt struct { 95 | Node string 96 | Port string 97 | Index int `json:",omitempty"` // ignored 98 | } 99 | Graph string 100 | Metadata map[string]interface{} `json:",omitempty"` // ignored 101 | } 102 | 103 | // removeEdge is a client message to delete a connection from a graph. 104 | type removeEdge struct { 105 | Src struct { 106 | Node string 107 | Port string 108 | } 109 | Tgt struct { 110 | Node string 111 | Port string 112 | } 113 | Graph string 114 | } 115 | 116 | // changeEdge is a client message to change connection metadata. 117 | type changeEdge struct { // ignored 118 | Src struct { 119 | Node string 120 | Port string 121 | Index int `json:",omitempty"` 122 | } 123 | Tgt struct { 124 | Node string 125 | Port string 126 | Index int `json:",omitempty"` 127 | } 128 | Graph string 129 | Metadata map[string]interface{} 130 | } 131 | 132 | // addInitial is a client message to add an IIP to a graph. 133 | type addInitial struct { 134 | Src struct { 135 | Data interface{} 136 | } 137 | Tgt struct { 138 | Node string 139 | Port string 140 | Index int `json:",omitempty"` // ignored 141 | } 142 | Graph string 143 | Metadata map[string]interface{} `json:",omitempty"` // ignored 144 | } 145 | 146 | // removeInitial is a client message to remove an IIP from a graph. 147 | type removeInitial struct { 148 | Tgt struct { 149 | Node string 150 | Port string 151 | Index int `json:",omitempty"` // ignored 152 | } 153 | Graph string 154 | } 155 | 156 | // addPort is a client message to add an exported inport/outport to the graph. 157 | type addPort struct { 158 | Public string 159 | Node string 160 | Port string 161 | Graph string 162 | Metadata map[string]interface{} `json:",omitempty"` // ignored 163 | } 164 | 165 | // removePort is a client message to remove an exported inport/outport from the graph. 166 | type removePort struct { 167 | Public string 168 | Graph string 169 | } 170 | 171 | // renamePort is a client message to rename a port of a graph. 172 | type renamePort struct { 173 | From string 174 | To string 175 | Graph string 176 | } 177 | 178 | type componentMessage struct { 179 | Protocol string `json:"protocol"` 180 | Command string `json:"command"` 181 | Payload ComponentInfo `json:"payload"` 182 | } 183 | -------------------------------------------------------------------------------- /runtime.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package goflow 4 | 5 | import ( 6 | "encoding/json" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/gorilla/websocket" 11 | "github.com/nu7hatch/gouuid" 12 | ) 13 | 14 | type protocolHandler func(*websocket.Conn, interface{}) 15 | 16 | // Runtime is a NoFlo-compatible runtime implementing the FBP protocol 17 | type Runtime struct { 18 | // Unique runtime ID for use with Flowhub 19 | id string 20 | // Protocol command handlers 21 | handlers map[string]protocolHandler 22 | // Graphs created at runtime and exposed as components 23 | graphs map[string]*Graph 24 | // Main graph ID 25 | mainId string 26 | // Main graph 27 | main *Graph 28 | // Websocket server onReady signal 29 | ready chan struct{} 30 | // Websocket server onShutdown signal 31 | done chan struct{} 32 | // Gorilla Webscocket upgrader 33 | upgrader websocket.Upgrader 34 | } 35 | 36 | func sendJSON(ws *websocket.Conn, msg interface{}) { 37 | bytes, err := json.Marshal(msg) 38 | if err != nil { 39 | log.Println("JSON encoding error", err) 40 | return 41 | } 42 | err = ws.WriteMessage(websocket.TextMessage, bytes) 43 | if err != nil { 44 | log.Println("Websocket write error", err) 45 | } 46 | } 47 | 48 | // Register command handlers 49 | func (r *Runtime) Init(name string) { 50 | uv4, err := uuid.NewV4() 51 | if err != nil { 52 | log.Println(err.Error()) 53 | } 54 | r.id = uv4.String() 55 | r.done = make(chan struct{}) 56 | r.ready = make(chan struct{}) 57 | r.handlers = make(map[string]protocolHandler) 58 | r.handlers["runtime.getruntime"] = func(ws *websocket.Conn, payload interface{}) { 59 | sendJSON(ws, runtimeMessage{ 60 | Protocol: "runtime", 61 | Command: "runtime", 62 | Payload: runtimeInfo{Type: name, 63 | Version: "0.4", 64 | Capabilities: []string{"protocol:runtime", 65 | "protocol:graph", 66 | "protocol:component", 67 | "protocol:network", 68 | "component:getsource"}, 69 | Id: r.id, 70 | }, 71 | }) 72 | } 73 | r.handlers["graph.clear"] = func(ws *websocket.Conn, payload interface{}) { 74 | msg := payload.(clearGraph) 75 | r.graphs[msg.Id] = new(Graph) 76 | r.graphs[msg.Id].InitGraphState() 77 | if msg.Main { 78 | r.mainId = msg.Id 79 | r.main = r.graphs[msg.Id] 80 | } 81 | if _, exists := ComponentRegistry[msg.Id]; !exists { 82 | Register(msg.Id, func() interface{} { 83 | net := new(Graph) 84 | net.InitGraphState() 85 | return net 86 | }) 87 | } 88 | Annotate(msg.Id, ComponentInfo{ 89 | Description: msg.Description, 90 | Icon: msg.Icon, 91 | }) 92 | UpdateComponentInfo(msg.Id) 93 | entry, _ := ComponentRegistry[msg.Id] 94 | sendJSON(ws, componentMessage{ 95 | Protocol: "component", 96 | Command: "component", 97 | Payload: entry.Info, 98 | }) 99 | } 100 | r.handlers["graph.addnode"] = func(ws *websocket.Conn, payload interface{}) { 101 | msg := payload.(addNode) 102 | r.graphs[msg.Graph].AddNew(msg.Component, msg.Id) 103 | } 104 | r.handlers["graph.removenode"] = func(ws *websocket.Conn, payload interface{}) { 105 | msg := payload.(removeNode) 106 | r.graphs[msg.Graph].Remove(msg.Id) 107 | } 108 | r.handlers["graph.renamenode"] = func(ws *websocket.Conn, payload interface{}) { 109 | msg := payload.(renameNode) 110 | r.graphs[msg.Graph].Rename(msg.From, msg.To) 111 | } 112 | r.handlers["graph.changenode"] = func(ws *websocket.Conn, payload interface{}) { 113 | // Currently unsupported 114 | } 115 | r.handlers["graph.addedge"] = func(ws *websocket.Conn, payload interface{}) { 116 | msg := payload.(addEdge) 117 | r.graphs[msg.Graph].Connect(msg.Src.Node, msg.Src.Port, msg.Tgt.Node, msg.Tgt.Port) 118 | } 119 | r.handlers["graph.removedge"] = func(ws *websocket.Conn, payload interface{}) { 120 | msg := payload.(removeEdge) 121 | r.graphs[msg.Graph].Disconnect(msg.Src.Node, msg.Src.Port, msg.Tgt.Node, msg.Tgt.Port) 122 | } 123 | r.handlers["graph.changeedge"] = func(ws *websocket.Conn, payload interface{}) { 124 | // Currently unsupported 125 | } 126 | r.handlers["graph.addinitial"] = func(ws *websocket.Conn, payload interface{}) { 127 | msg := payload.(addInitial) 128 | r.graphs[msg.Graph].AddIIP(msg.Src.Data, msg.Tgt.Node, msg.Tgt.Port) 129 | } 130 | r.handlers["graph.removeinitial"] = func(ws *websocket.Conn, payload interface{}) { 131 | msg := payload.(removeInitial) 132 | r.graphs[msg.Graph].RemoveIIP(msg.Tgt.Node, msg.Tgt.Port) 133 | } 134 | r.handlers["graph.addinport"] = func(ws *websocket.Conn, payload interface{}) { 135 | msg := payload.(addPort) 136 | r.graphs[msg.Graph].MapInPort(msg.Public, msg.Node, msg.Port) 137 | UpdateComponentInfo(msg.Graph) 138 | entry, _ := ComponentRegistry[msg.Graph] 139 | sendJSON(ws, componentMessage{ 140 | Protocol: "component", 141 | Command: "component", 142 | Payload: entry.Info, 143 | }) 144 | } 145 | r.handlers["graph.removeinport"] = func(ws *websocket.Conn, payload interface{}) { 146 | msg := payload.(removePort) 147 | r.graphs[msg.Graph].UnsetInPort(msg.Public) 148 | r.graphs[msg.Graph].UnmapInPort(msg.Public) 149 | UpdateComponentInfo(msg.Graph) 150 | entry, _ := ComponentRegistry[msg.Graph] 151 | sendJSON(ws, componentMessage{ 152 | Protocol: "component", 153 | Command: "component", 154 | Payload: entry.Info, 155 | }) 156 | } 157 | r.handlers["graph.renameinport"] = func(ws *websocket.Conn, payload interface{}) { 158 | msg := payload.(renamePort) 159 | r.graphs[msg.Graph].RenameInPort(msg.From, msg.To) 160 | UpdateComponentInfo(msg.Graph) 161 | entry, _ := ComponentRegistry[msg.Graph] 162 | sendJSON(ws, componentMessage{ 163 | Protocol: "component", 164 | Command: "component", 165 | Payload: entry.Info, 166 | }) 167 | } 168 | r.handlers["graph.addoutport"] = func(ws *websocket.Conn, payload interface{}) { 169 | msg := payload.(addPort) 170 | r.graphs[msg.Graph].MapOutPort(msg.Public, msg.Node, msg.Port) 171 | UpdateComponentInfo(msg.Graph) 172 | entry, _ := ComponentRegistry[msg.Graph] 173 | sendJSON(ws, componentMessage{ 174 | Protocol: "component", 175 | Command: "component", 176 | Payload: entry.Info, 177 | }) 178 | } 179 | r.handlers["graph.removeoutport"] = func(ws *websocket.Conn, payload interface{}) { 180 | msg := payload.(removePort) 181 | r.graphs[msg.Graph].UnsetOutPort(msg.Public) 182 | r.graphs[msg.Graph].UnmapOutPort(msg.Public) 183 | UpdateComponentInfo(msg.Graph) 184 | entry, _ := ComponentRegistry[msg.Graph] 185 | sendJSON(ws, componentMessage{ 186 | Protocol: "component", 187 | Command: "component", 188 | Payload: entry.Info, 189 | }) 190 | } 191 | r.handlers["graph.renameoutport"] = func(ws *websocket.Conn, payload interface{}) { 192 | msg := payload.(renamePort) 193 | r.graphs[msg.Graph].RenameOutPort(msg.From, msg.To) 194 | UpdateComponentInfo(msg.Graph) 195 | entry, _ := ComponentRegistry[msg.Graph] 196 | sendJSON(ws, componentMessage{ 197 | Protocol: "component", 198 | Command: "component", 199 | Payload: entry.Info, 200 | }) 201 | } 202 | r.handlers["component.list"] = func(ws *websocket.Conn, payload interface{}) { 203 | for key, entry := range ComponentRegistry { 204 | if len(entry.Info.InPorts) == 0 && len(entry.Info.OutPorts) == 0 { 205 | // Need to obtain ports annotation for the first time 206 | UpdateComponentInfo(key) 207 | } 208 | sendJSON(ws, componentMessage{ 209 | Protocol: "component", 210 | Command: "component", 211 | Payload: entry.Info, 212 | }) 213 | } 214 | } 215 | } 216 | 217 | // Id returns runtime's UUID v4 218 | func (r *Runtime) Id() string { 219 | return r.id 220 | } 221 | 222 | // Ready returns a channel which is closed when the runtime is ready to work 223 | func (r *Runtime) Ready() chan struct{} { 224 | return r.ready 225 | } 226 | 227 | // Stop tells the runtime to shut down 228 | func (r *Runtime) Stop() { 229 | close(r.done) 230 | } 231 | 232 | func (r *Runtime) Handle(w http.ResponseWriter, req *http.Request) { 233 | ws, err := r.upgrader.Upgrade(w, req, nil) 234 | if err != nil { 235 | log.Println("Websocket upgrader failed", err) 236 | return 237 | } 238 | defer ws.Close() 239 | for { 240 | msgType, bytes, err := ws.ReadMessage() 241 | if err != nil { 242 | log.Println("Websocket read error:", err) 243 | break 244 | } 245 | if msgType != websocket.TextMessage { 246 | log.Println("Unexpected binary message") 247 | break 248 | } 249 | var msg Message 250 | err = json.Unmarshal(bytes, &msg) 251 | if err != nil { 252 | log.Println("JSON decoding error:", err) 253 | break 254 | } 255 | handler, exists := r.handlers[msg.Protocol+"."+msg.Command] 256 | if !exists { 257 | log.Printf("Unknown command: %s.%s\n", msg.Protocol, msg.Command) 258 | break 259 | } 260 | handler(ws, msg.Payload) 261 | } 262 | } 263 | 264 | func (r *Runtime) Listen(address string) { 265 | r.upgrader = websocket.Upgrader{} 266 | 267 | http.Handle("/", http.HandlerFunc(r.Handle)) 268 | 269 | go func() { 270 | log.Fatal(http.ListenAndServe(address, nil)) 271 | }() 272 | close(r.ready) 273 | 274 | // Wait for termination signal 275 | <-r.done 276 | } 277 | -------------------------------------------------------------------------------- /runtime_test.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package goflow 4 | 5 | import ( 6 | "encoding/json" 7 | "testing" 8 | 9 | "github.com/gorilla/websocket" 10 | ) 11 | 12 | var ( 13 | r *Runtime 14 | started bool 15 | ) 16 | 17 | func ensureRuntimeStarted() { 18 | if !started { 19 | r = new(Runtime) 20 | r.Init("goflow") 21 | go r.Listen("localhost:13014") 22 | started = true 23 | <-r.Ready() 24 | } 25 | } 26 | 27 | func sendJSONE(ws *websocket.Conn, msg interface{}) error { 28 | bytes, err := json.Marshal(msg) 29 | if err != nil { 30 | return err 31 | } 32 | err = ws.WriteMessage(websocket.TextMessage, bytes) 33 | return err 34 | } 35 | 36 | // Tests runtime information support 37 | func TestRuntimeGetRuntime(t *testing.T) { 38 | ensureRuntimeStarted() 39 | // Create a WebSocket client 40 | ws, _, err := websocket.DefaultDialer.Dial("ws://localhost:13014/", nil) 41 | defer ws.Close() 42 | if err != nil { 43 | t.Error(err.Error()) 44 | } 45 | // Send a runtime request and check the response 46 | if err = sendJSONE(ws, &Message{"runtime", "getruntime", nil}); err != nil { 47 | t.Error(err.Error()) 48 | } 49 | var msg runtimeMessage 50 | var bytes []byte 51 | if _, bytes, err = ws.ReadMessage(); err != nil { 52 | t.Error(err.Error()) 53 | return 54 | } 55 | if err = json.Unmarshal(bytes, &msg); err != nil { 56 | t.Error(err.Error()) 57 | return 58 | } 59 | if msg.Protocol != "runtime" || msg.Command != "runtime" { 60 | t.Errorf("Invalid protocol (%s) or command (%s)", msg.Protocol, msg.Command) 61 | return 62 | } 63 | res := msg.Payload 64 | if res.Type != "goflow" { 65 | t.Errorf("Invalid protocol type: %s\n", res.Type) 66 | } 67 | if res.Version != "0.4" { 68 | t.Errorf("Invalid protocol version: %s\n", res.Version) 69 | } 70 | if len(res.Capabilities) != 5 { 71 | t.Errorf("Invalid number of supported capabilities: %v\n", res.Capabilities) 72 | } 73 | if res.Id == "" { 74 | t.Error("Runtime Id is empty") 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test_codecov.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | echo "" > coverage.txt 5 | 6 | for d in $(go list ./... | grep -v vendor); do 7 | go test -v -coverprofile=profile.out -covermode=atomic "$d" 8 | if [ -f profile.out ]; then 9 | cat profile.out >> coverage.txt 10 | rm profile.out 11 | fi 12 | done 13 | --------------------------------------------------------------------------------