{"id":16443402,"url":"https://github.com/0exp/symbiont-ruby","last_synced_at":"2025-03-21T05:30:33.021Z","repository":{"id":49241637,"uuid":"112659767","full_name":"0exp/symbiont-ruby","owner":"0exp","description":"Invoke proc-objects in many contexts simultaneously. Provides a controllable technique to intercept and dispatch methods inside proc object (or inside a series of proc objects)","archived":false,"fork":false,"pushed_at":"2024-11-13T22:45:23.000Z","size":6482,"stargazers_count":10,"open_issues_count":3,"forks_count":4,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-17T21:42:41.189Z","etag":null,"topics":["dsl","magic","ruby-dsl","ruby-lambda","ruby-proc"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/0exp.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-11-30T20:58:01.000Z","updated_at":"2024-11-13T22:45:28.000Z","dependencies_parsed_at":"2024-06-21T15:19:59.915Z","dependency_job_id":"10304d8d-788f-4750-8659-d32e321a681c","html_url":"https://github.com/0exp/symbiont-ruby","commit_stats":{"total_commits":96,"total_committers":5,"mean_commits":19.2,"dds":"0.42708333333333337","last_synced_commit":"fc14f700bf3a894f9566b1580a10f3cc3da14d56"},"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0exp%2Fsymbiont-ruby","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0exp%2Fsymbiont-ruby/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0exp%2Fsymbiont-ruby/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0exp%2Fsymbiont-ruby/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/0exp","download_url":"https://codeload.github.com/0exp/symbiont-ruby/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244745621,"owners_count":20503042,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dsl","magic","ruby-dsl","ruby-lambda","ruby-proc"],"created_at":"2024-10-11T09:20:21.144Z","updated_at":"2025-03-21T05:30:31.315Z","avatar_url":"https://github.com/0exp.png","language":"Ruby","readme":"\u003cp align=\"center\"\u003e\u003cimg width=\"250\" height=\"250\" src=\"logo/symbiont_logo_circle.png\" /\u003e\u003c/div\u003e\n\u003cp align=\"center\"\u003e\n  \u003ch1 align=\"center\"\u003e\n    Symbiont\u003cbr /\u003e\n    \u003ca href=\"https://badge.fury.io/rb/symbiont-ruby\"\u003e\u003cimg src=\"https://badge.fury.io/rb/symbiont-ruby.svg\"\u003e\u003c/a\u003e\n  \u003c/h1\u003e\n\u003c/p\u003e\n\n**Symbiont** is a cool implementation of proc-objects execution algorithm: in the context of other object,\nbut with the preservation of the captured environment of the proc object and with the ability of control the method dispatch\ninside it. A proc object is executed in three contexts: in the context of required object, in the context of\na captured proc's environment and in the global (Kernel) context.\n\n# Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'symbiont-ruby'\n```\n\nAnd then execute:\n\n```shell\n$ bundle install\n```\n\nOr install it yourself as:\n\n```shell\n$ gem install symbiont-ruby\n```\n\nAfter all:\n\n```ruby\nrequire 'symbiont'\n```\n\n# Table of contents\n\n- [Problem and motivation](#problems-and-motivaiton)\n- [Usage](#usage)\n  - [Context management](#context-management)\n  - [Supported method delegation directions](#supported-methods-delegation-directions)\n  - [Proc object invocation](#proc-object-invocation)\n    - [Considering public methods only (.evaluate)](#considering-public-methods-only-evaluate)\n    - [Considering private and public methods (.evaluate_private)](#considering-private-and-public-methods-evaluate_private)\n  - [Getting method-objects (Method)](#getting-method-objects-method)\n    - [Considering public methods only (.public_method)](#considering-public-methods-only-public_method)\n    - [Considering public and private methods (.private_method)](#considering-public-and-private-methods-private_method)\n  - [Symbiont Mixin](#symbiont-mixin)\n    - [Mixing a module with default delegation direction](#mixing-a-module-with-default-delegation-direction)\n    - [Mixing a module with certain delegation direction](#mixing-a-module-with-certain-delegation-direction)\n  - [Multiple inner contexts](#multiple-inner-contexts)\n  - [Isolator - proc object isolation layer for delayed invocations](#isolator---proc-object-isolation-layer-for-delayed-invocations)\n\n# Problems and motivaiton\n\nThe main problem of `instance_eval` / `instance exec` / `class_eval` / `class_exec` is that the binding (self)\ninside a proc-object is replaced with the object in where a proc object is executed. This allows you to delegate all methods captured by a proc to another object.\nBut this leads to the fact that the proc object loses access to the original captured environment.\nSymbiont solves this problem by allowing the proc to be executed in the required context while maintaining access to the methods of the captured environment\n(including the global context).\n\n---\n\nA problem with `instance_eval` / `instance_exec` / `class_eval` / `class_exec`:\n\n```ruby\nclass TableFactory\n  def initialize\n    @primary_key = nil\n  end\n\n  def primary_key(key)\n    @primary_key = key\n  end\nend\n\nclass Migration\n  class \u003c\u003c self\n    def create_table(\u0026block)\n      TableFactory.new.tap do |table|\n        table.instance_eval(\u0026block) # NOTE: closure invocation\n      end\n    end\n  end\nend\n\nclass CreateUsers \u003c Migration\n  class \u003c\u003c self\n    def up\n      create_table do # NOTE: failing closure\n        primary_key(generate_pk(:id))\n      end\n    end\n\n    def generate_pk(name)\n      \"secure_#{name}\"\n    end\n  end\nend\n\nCreateUsers.up\n# =\u003e NoMethodError: undefined method `generate_pk' for #\u003cTableFactory:0x00007f8560ca4a58\u003e\n```\n\nSymbiont solves this:\n\n```ruby\nrequire 'symbiont'\n\nclass Migration\n  class \u003c\u003c self\n    def create_table(\u0026block)\n      TableFactory.new.tap do |table|\n        Symbiont::Executor.evaluate(table, \u0026block) # NOTE: intercept closure invocation by Symbiont\n      end\n    end\n  end\nend\n\nCreateUsers.up\n# =\u003e #\u003cTableFactory:0x00007f990527f268 @primary_key=\"secure_id\"\u003e\n```\n\n---\n\nProc-object is executed in three contexts at the same time:\n\n- in the context of closure;\n- in the context of required object;\n- in the global context (Kernel).\n\nMethods (called internally) are delegated to the context that is first able to respond.\nThe order of context selection depends on the corresponding context direction parameter.\nBy default the delegation order is: object context =\u003e closure context =\u003e global context.\nIf no context is able to respond to the method, an exception is raised (`Symbiont::Trigger::ContextNoMethodError`).\nSymbiont can consider the visibility of methods when executing.\n\n\n# Usage\n\n## Context management\n\nImagine having the following code and the corresponding clojure in the real application:\n\n```ruby\ndef object_data\n  'outer_context'\nend\n\nclass SimpleObject\n  def format_data(data)\n    \"Data: #{data}\"\n  end\n\n  def object_data\n    'inner_context'\n  end\nend\n\nclass \u003c\u003c Kernel\n  def object_data\n    'kernel_data'\n  end\nend\n\nobject = SimpleObject.new\n\nclosure = proc { format_data(object_data) } # NOTE: our closure\n```\n\nHow a proc object will be processed, which context will be selected, how to make sure that nothing is broken - welcome to Symbiont.\n\n## Supported methods delegation directions\n\nDelegation order might be passed as a parameter to the proc execution\nand to the special mixin module, allowing any class or instance to become a symbiont.\n\n- Supported contexts:\n  - inner context - an object where proc is executed;\n  - outer context - external environment of the proc object;\n  - kernel context - global Kernel context.\n\nSymbiont::IOK is chosen by default (`inner context =\u003e outer context =\u003e kernel context`).\n\n```ruby\nSymbiont::IOK # Inner Context  =\u003e Outer Context  =\u003e Kernel Context (DEFAULT)\nSymbiont::OIK # Outer Context  =\u003e Inner Context  =\u003e Kernel Context\nSymbiont::OKI # Outer Context  =\u003e Kernel Context =\u003e Inner Context\nSymbiont::IKO # Inner Context  =\u003e Kernel Context =\u003e Outer Context\nSymbiont::KOI # Kernel Context =\u003e Outer Context  =\u003e Inner Context\nSymbiont::KIO # Kernel Context =\u003e Inner Context  =\u003e Outer Context\n```\n\n## Proc object invocation\n\n`Symbiont::Executor` allows you to execute proc objects in two modes of the delegation:\n\n- only public methods:\n  - `evaluate(*required_contexts, [context_direction:], \u0026closure)`\n- public and private methods:\n  - `evaluate_private(*required_contexts, [context_direction:], \u0026closure)`\n\nIf none of contexts is able to respond to the required method - `Symbiont::Trigger::ContextNoMethodError` exception is raised.\n\nIn the case an unsupported direction value is used - `Symbiont::Trigger::IncompatibleContextDirectionError` exception is raised.\n\nIf proc object isn't passed to the executor - `Symbiont::Trigger::UnprovidedClosureAttributeError` exception is raised.\n\n#### Considering public methods only (.evaluate)\n\n```ruby\n# with default delegation order (Symbiont::IOK)\nSymbiont::Executor.evaluate(object) do\n  format_data(object_data)\nend\n# =\u003e \"Data: inner_context\"\n\n# with a custom delegation order\nSymbiont::Executor.evaluate(object, context_direction: Symbiont::KIO) do\n  format_data(object_data)\nend\n# =\u003e \"Data: kernel_context\"\n\n# SimpleObject#object_data is a private method (inner_context)\nSymbiont::Executor.evaluate(object, context_direction: Symbiont::IOK) do\n  format_data(object_data)\nend\n# =\u003e \"Data: outer_context\"\n```\n\n#### Considering private and public methods (.evaluate_private)\n\n```ruby\n# with default delegation order (Symbiont::IOK)\nSymbiont::Executor.evaluate_private(object) do\n  format_data(object_data)\nend\n# =\u003e \"Data: inner_context\"\n\n# with a custom delegation order\nSymbiont::Executor.evaluate_private(object, context_direction: Symbiont::KIO) do\n  format_data(object_data)\nend\n# =\u003e \"Data: kernel_context\"\n\n# SimpleObject#object_data is a private method (inner_context)\nSymbiont::Executor.evaluate_private(object, context_direction: Symbiont::IOK) do\n  format_data(object_data)\nend\n# =\u003e \"Data: inner_data\"\n```\n\n## Getting method-objects (Method)\n\n`Symbiont::Executor` provides the possibility of obtaining the method object with consideration of the chosen delegation order:\n\n- only public methods:\n  - `public_method(method_name, *required_contexts, [context_direction:], \u0026clojure)`\n- public and private methods:\n  - `private_method(method_name, *required_contexts, [context_direction:], \u0026clojure)`\n\nIf none of contexts is able to respond to the required method - `Symbiont::Trigger::ContextNoMethodError` exception is raised.\n\nIn the case an unsupported direction value is used - `Symbiont::Trigger::IncompatibleContextDirectionError` exception is raised.\n\n#### Considering public methods only (.public_method)\n\n```ruby\n# with default delegation order (Symbiont::IOK)\nSymbiont::Executor.public_method(:object_data, object, \u0026closure)\n# =\u003e #\u003cMethod: SimpleObject#object_data\u003e\n\n# with a custom delegation order\nSymbiont::Executor.public_method(:object_data, object, context_direction: Symbiont::OIK, \u0026closure)\n# =\u003e (main) #\u003cMethod: SimpleObject(object)#object_data\u003e\n\n# SimpleObject#object_data is a private method\nSymbiont::Executor.public_method(:object_data, object, context_direction: Symbiont::IOK, \u0026closure)\n# =\u003e (main) #\u003cMethod: SimpleObject(object)#object_data\u003e\n```\n\n#### Considering public and private methods (.private_method)\n\n```ruby\n# with default delegation order (Symbiont::IOK)\nSymbiont::Executor.private_method(:object_data, object, \u0026clojure)\n# =\u003e #\u003cMethod: SimpleObject#object_data\u003e\n\n# with a custom delegation order\nSymbiont::Executor.private_method(:object_data, object, context_direction: Symbiont::KIO, \u0026clojure)\n# =\u003e #\u003cMethod: Kernel.object_data\u003e\n\n# SimpleObject#object_data is a private_method\nSymbiont::Executor.private_method(:object_data, object, context_direction: Symbiotn::IKO, \u0026clojure)\n# =\u003e #\u003cMethod: SimpleObject#object_data\u003e\n```\n\n## Symbiont Mixin\n\n`Symbiont::Context` is a mixin that allows any object to call proc objects in the context of itself as `Symbiont::Executor`.\n\nYou can specify the direction of the context delegation. `Symbiont::IOK` is used by default.\n\n#### Mixing a module with default delegation direction\n\n```ruby\nclass SimpleObject\n  include Symbiont::Context # Symbiont::IOK direction is used by default\n\n  # #evaluate([context_direction = Symbiont::IOK], \u0026closure)\n  # #evaluate_private([context_direction = Symbiont::IOK], \u0026closure)\n  # #public_method(method_name, [context_direction = Symbiont::IOK])\n  # #private_method(method_name, [context_direction = Symbiont::IOK])\n\n  extend Symbiont::Context # Symbiont::IOK direction is used by default\n\n  # .evaluate([context_direction = Symbiont::IOK], \u0026closure)\n  # .evaluate_private([context_direction = Symbiont::IOK], \u0026closure)\n  # .public_method(method_name, [context_direction = Symbiont::IOK])\n  # .private_method(method_name, [context_direction = Symbiont::IOK])\nend\n\ndef object_data\n  'outer_context'\nend\n\nSimpleObject.new.evaluate { object_data }\n# =\u003e object.object_data =\u003e \"inner_context\"\n\nSimpleObject.new.evaluate(Symbiont::OIK) { object_data }\n# =\u003e object_data() =\u003e \"outer_context\"\n```\n\n#### Mixing a module with certain delegation direction\n\n```ruby\nclass SimpleObject\n  include Symbiont::Context(Symboiont::KOI) # use a custom direction\n\n  # #evaluate([context_direction = Symbiont::KOI], \u0026closure)\n  # #evaluate_private([context_direction = Symbiont::KOI], \u0026closure)\n  # #public_method(method_name, [context_direction = Symbiont::KOI])\n  # #private_method(method_name, [context_direction = Symbiont::KOI])\n\n  extend Symbiont::Context(Symbiont::KOI) # use a custom direction\n\n  # .evaluate([context_direction = Symbiont::KOI], \u0026closure)\n  # .evaluate_private([context_direction = Symbiont::KOI], \u0026closure)\n  # .public_method(method_name, [context_direction = Symbiont::KOI])\n  # .private_method(method_name, [context_direction = Symbiont::KOI])\nend\n\nSimpleObject.new.evaluate { object_data }\n# =\u003e Kernel.object_data =\u003e \"kernel_context\"\n\nSimpleObject.new.evaluate(Symbiont::IOK) { object_data }\n# =\u003e object.object_data =\u003e \"inner_context\"\n```\n\n## Multiple inner contexts\n\n\n`Symbiont::Executor` allows you to work with multiple inner contexts (can receive a set of objects instead of the one main object).\nEach object will be used as an inner context in order they are passed.\nThe method will be addressed to the object that responds first (according to a chosen delegation order).\n\n```ruby\n# Usage:\n\nSymbiont::Executor.evaluate(object_a, object_b, context_direction: Symbiont::IOK, \u0026closure)\nSymbiont::Executor.evaluate_private(object_a, object_b, context_direction: Symbiont::IOK, \u0026closure)\nSymbiont::Executor.publc_method(:method_name, object_a, object_b, context_direction: Symbiont::IOK, \u0026closure)\nSymbiont::Executor.private_method(:method_name, object_a, object_b, context_direction: Symbiont::IOK, \u0026closure)\n\n# Example\n\nobject_a.info # =\u003e \"object_info\"\nobject_b.data # =\u003e \"object_data\"\n\nclosure = proc { \"#{info} #{data}\" }\n\nSymbiont::Executor.evaluate(object_a, object_b, \u0026closure) # =\u003e \"object_info object_data\"\nSymbiont::Executor.public_method(:data, object_a, object_b, \u0026closure).call # =\u003e \"object_data\"\nSymbiont::Executor.public_method(:info, object_a, object_b, \u0026closure).call # =\u003e \"object_info\"\n```\n\n## Isolator - proc object isolation layer for delayed invocations\n\n`Symbiont::Isolator` is a special object that wraps your proc object from any place and provides\nan ability to invoke this proc object lazily inside an any series of contexts.\nAll `Symbiont::Executor` features are supported (by the way, `Symbiont::Executor`\nuses `Symbiont::Isolator` under the hood).\n\n```ruby\n# Usage:\n\n# with default direction (Symbiont::IOK)\nisolator = Symbiont::Isolator.new { call_any_method }\n\n# with custom direction\nisolator = Symbiont::Isolator.new(default_direction: Symbiont::KIO) { call_any_method }\n\n# invocation\nisolator.evaluate(object_a, object_b) # use default direction defined on instantiation\nisolator.evaluate(object_a, object_b, direction: Symbiont::KOI) # use custom direction\n# same for #.evaluate_private\n\n# getting a method object\nisolator.public_method(:call_any_method, object_a, object_b) # use default direction defined on instantiation\nisolator.public_method(:call_any_method, object_a, object_b, direction: Symbiont::KIO) # use custom direction\nisolator.private_method(...)\n# same for #.private_method\n```\n\n# Roadmap\n\n- support for toplevel context (`TOPLEVEL_BINDING`);\n- `fiber teleports`;\n- official support for **Ruby@3**;\n\n# Build\n\n```shell\nbundle exec rake rubocop\nbundle exec rake yardoc\nbundle exec rake rspec\n```\n\n# Contributing\n\n- Fork it ( https://github.com/0exp/symbiont-ruby/fork )\n- Create your feature branch (`git checkout -b my-new-feature`)\n- Commit your changes (`git commit -am 'Add some feature'`)\n- Push to the branch (`git push origin my-new-feature`)\n- Create new Pull Request\n\n# License\n\nReleased under MIT License.\n\n# Authors\n\n[Logo](https://github.com/0exp/symbiont-ruby/tree/master/logo) was created by **Viktoria Izmaylova** (my special thanks ^_^).\n\nProject was created by **Rustam Ibragimov**.\n\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F0exp%2Fsymbiont-ruby","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F0exp%2Fsymbiont-ruby","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F0exp%2Fsymbiont-ruby/lists"}