{"id":13806155,"url":"https://github.com/t6d/composable_operations","last_synced_at":"2025-05-13T21:32:29.697Z","repository":{"id":8880962,"uuid":"10597684","full_name":"t6d/composable_operations","owner":"t6d","description":"Composable Operations is a tool set for creating operations and assembling multiple of these operations in operation pipelines.","archived":true,"fork":false,"pushed_at":"2016-09-23T20:41:15.000Z","size":116,"stargazers_count":46,"open_issues_count":5,"forks_count":7,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-05-12T07:19:59.287Z","etag":null,"topics":[],"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/t6d.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2013-06-10T10:04:53.000Z","updated_at":"2025-04-06T18:59:28.000Z","dependencies_parsed_at":"2022-08-24T13:00:19.834Z","dependency_job_id":null,"html_url":"https://github.com/t6d/composable_operations","commit_stats":null,"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/t6d%2Fcomposable_operations","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/t6d%2Fcomposable_operations/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/t6d%2Fcomposable_operations/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/t6d%2Fcomposable_operations/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/t6d","download_url":"https://codeload.github.com/t6d/composable_operations/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254031316,"owners_count":22002745,"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":[],"created_at":"2024-08-04T01:01:08.381Z","updated_at":"2025-05-13T21:32:29.438Z","avatar_url":"https://github.com/t6d.png","language":"Ruby","readme":"# ComposableOperations\n\nComposable Operations is a tool set for creating operations and assembling\nmultiple of these operations in operation pipelines.  An operation is, at its\ncore, an implementation of the [strategy\npattern](http://en.wikipedia.org/wiki/Strategy_pattern) and in this sense an\nencapsulation of an algorithm. An operation pipeline is an assembly of multiple\noperations and useful for implementing complex algorithms. Pipelines themselves\ncan be part of other pipelines.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n    gem 'composable_operations'\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install composable_operations\n\n## Usage\n\nOperations can be defined by subclassing `ComposableOperations::Operation` and\noperation pipelines by subclassing `ComposableOperations::ComposedOperation`.\n\n### Defining an Operation\n\nTo define an operation, two steps are necessary:\n\n1. create a new subclass of `ComposableOperations::Operations`, and\n2. implement the `#execute` method.\n\nThe listing below shows an operation that extracts a timestamp in the format\n`yyyy-mm-dd` from a string.\n\n```ruby\nclass DateExtractor \u003c ComposableOperations::Operation\n\n  processes :text\n\n  def execute\n    text.scan(/(\\d{4})-(\\d{2})-(\\d{2})/)\n  end\n\nend\n```\n\nThe macro method `.processes` followed by a single argument denotes that the\noperation expects a single object as input and results in the definition of a\ngetter method named as specified by this argument. The macro method can also be\ncalled with multiple arguments resulting in the creation of multiple getter\nmethods. The latter is useful if the operation requires more than one object as\ninput to operate. Calling the macro method is entirely optional. An operation's\ninput can always be accessed by calling the getter method `#input`. This method\nreturns either a single object or an array of objects.\n\nThere are two ways to execute this operation:\n\n1. create a new instance of this operation and call `#perform`, or\n2. directly call `.perform` on the operation class.\n\nThe major difference between these two approaches is that in case of a failure\nthe latter raises an exception while the former returns `nil` and sets the\noperation's state to `failed`. For more information on canceling the execution\nof an operation, see below. Please note that directly calling the `#execute`\nmethod is prohibited. To enforce this constraint, the method is automatically\nmarked as protected upon definition.\n\nThe listing below demonstrates how to execute the operation defined above.\n\n```ruby\ntext = \"This gem was first published on 2013-06-10.\"\n\nextractor = DateExtractor.new(text)\nextractor.perform # =\u003e [[\"2013\", \"06\", \"10\"]]\n\nDateExtractor.perform(text) # =\u003e [[\"2013\", \"06\", \"10\"]]\n```\n\n### Defining an Operation Pipeline\n\nAssume that we are provided an operation that converts these arrays of strings\ninto actual `Time` objects. The following listing provides a potential\nimplementation of such an operation.\n\n```ruby\nclass DateArrayToTimeObjectConverter \u003c ComposableOperations::Operation\n\n  processes :collection_of_date_arrays\n\n  def execute\n    collection_of_date_arrays.map do |date_array|\n      Time.new(*(date_array.map(\u0026:to_i)))\n    end\n  end\n\nend\n```\n\nUsing these two operations, it is possible to create a composed operation that\nextracts dates from a string and directly converts them into `Time` objects. To\ndefine a composed operation, two steps are necessary:\n\n1. create a subclass of `ComposableOperations::ComposedOperation`, and\n2. use the macro method `use` to assemble the operation.\n\nThe listing below shows how to assemble the two operations, `DateExtractor` and\n`DateArrayToTimeObjectConverter`, into a composed operation named `DateParser`.\n\n```ruby\nclass DateParser \u003c ComposableOperations::ComposedOperation\n\n  use DateExtractor\n  use DateArrayToTimeObjectConverter\n\nend\n```\n\nComposed operations provide the same interface as normal operations. Hence,\nthey can be invoked the same way. For the sake of completeness, the listing\nbelow shows how to use the `DateParser` operation.\n\n```ruby\ntext = \"This gem was first published on 2013-06-10.\"\n\nparser = DateParser.new(text)\nparser.perform # =\u003e 2013-06-07 00:00:00 +0200\n\nDateParser.perform(text) # =\u003e 2013-06-07 00:00:00 +0200\n```\n\n### Control Flow\n\nAn operation can be *halted* or *aborted* if a successful execution is not\npossible. Aborting an operation will result in an exception if the operation\nwas invoked using the class method `.perform`. If the operation was invoked\nusing the instance method `#perform`, the operation's state will be updated\naccordingly, but no exception will be raised. The listing below provides, among\nother things, examples on how to access an operation's state.\n\n```ruby\nclass StrictDateParser \u003c DateParser\n\n  def execute\n    result = super\n    fail \"no timestamp found\" if result.empty?\n    result\n  end\n\nend\n\nclass LessStrictDateParser \u003c DateParser\n\n  def execute\n    result = super\n    halt \"no timestamp found\" if result.empty?\n    result\n  end\n\nend\n\nparser = StrictDateParser.new(\"\")\nparser.message # =\u003e \"no timestamp found\"\nparser.perform # =\u003e nil\nparser.succeeded? # =\u003e false\nparser.halted? # =\u003e false\nparser.failed? # =\u003e true\n\nStrictDateParser.perform(\"\") # =\u003e ComposableOperations::OperationError: no timestamp found\n\nparser = LessStricDateParser.new(\"\")\nparser.message # =\u003e \"no timestamp found\"\nparser.perform # =\u003e nil\nparser.succeeded? # =\u003e false\nparser.halted? # =\u003e true\nparser.failed? # =\u003e false\n\nStrictDateParser.perform(\"\") # =\u003e nil\n```\n\nInstead of cluttering the `#execute` method with sentinel code or in general\nwith code that is not part of an operation's algorithmic core, we can move this\ncode into `before` or `after` callbacks. The listing below provides an alternative\nimplementation of the `StrictDateParser` operation.\n\n\n```ruby\nclass StrictDateParser \u003c DateParser\n\n  after do\n    fail \"no timestamp found\" if result.empty?\n  end\n\nend\n\nparser = StrictDateParser.new(\"\")\nparser.message # =\u003e \"no timestamp found\"\nparser.perform # =\u003e nil\nparser.failed? # =\u003e true\n\nStrictDateParser.perform(\"\") # =\u003e ComposableOperations::OperationError: no timestamp found\n```\n\n### Configuring Operations\n\nOperations and composed operations support\n[SmartProperties](http://github.com/t6d/smart_properties) to conveniently\nprovide additional settings upon initialization of an operation. In the\nexample below, an operation is defined that indents a given string. The indent\nis set to 2 by default but can easily be changed by supplying an options hash\nto the initializer.\n\n```ruby\nclass Indention \u003c ComposableOperations::Operation\n\n  processes :text\n\n  property :indent, default: 2,\n                    converts: lambda { |value| value.to_s.to_i },\n                    accepts: lambda { |value| value \u003e= 0 },\n                    required: true\n\n  def execute\n    text.split(\"\\n\").map { |line| \" \" * indent + line }.join(\"\\n\")\n  end\n\nend\n\nIndention.perform(\"Hello World\", indent: 4) # =\u003e \"    Hello World\"\n```\n\nOperations that are part of an composed operation can be configured by calling\nthe `.use` method with a hash of options as the second argument. See the\nlisting below for an example.\n\n```ruby\nclass SomeComposedOperation \u003c ComposableOperations::ComposedOperation\n\n  # ...\n  use Indention, indent: 4\n  # ...\n\n end\n```\n\n## Contributing\n\n1. Fork it\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create new Pull Request\n","funding_links":[],"categories":["NLP Pipeline Subtasks","Ruby"],"sub_categories":["Pipeline Generation"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ft6d%2Fcomposable_operations","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ft6d%2Fcomposable_operations","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ft6d%2Fcomposable_operations/lists"}