active_operator 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6b9cf2ed21ea684d81d26477429ede619d17b88eac2d5f84684419221a026a2b
4
+ data.tar.gz: 644dc660407b17b845086e62004ed5f0d7d0ba29ba31bb81abe801480124ef2e
5
+ SHA512:
6
+ metadata.gz: f7efb4a3cb1688678025b2a55124dc0b3c75da573d8c9b4b6bc093e01871cdd2470b3f7357fcf6d360c4bb559ba40cdbe6f4d2ffe2e8fffcba1cdb92b6ad2b5e
7
+ data.tar.gz: 4bca940a6bd59dd0c8d3bd27d33b10986ed1f932e20248a9674e9c8e1ac8520dbedce44c03910d4d920e907a11dd28fec2b840bc1f71fe2946cbcb3ee68d8807
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2025-07-05
4
+
5
+ - Initial release
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Jeremy Smith
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # Active Operator
2
+
3
+ A Rails pattern for calling external APIs, then storing and processing their responses.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem "active_operator"
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```bash
16
+ bundle install
17
+ rails generate active_operator:install
18
+ rails db:migrate
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ### 1. Define an Operation
24
+
25
+ Create operation classes that inherit from `ApplicationOperation`:
26
+
27
+ ```ruby
28
+ class Geocoding::V1 < ApplicationOperation
29
+ def request
30
+ # Make API call and return response, which will be stored in the operation record
31
+ faraday.get(
32
+ "https://api.geocod.io/v1.8/geocode",
33
+ {
34
+ q: record.address,
35
+ api_key: Rails.application.credentials.dig(:geocodio, :api_key),
36
+ fields: "timezone"
37
+ }
38
+ )
39
+ end
40
+
41
+ def process
42
+ # Load the response stored in the operation record, and update perform updates and other actions
43
+ result = response.dig("body", "results", 0)
44
+
45
+ record.update!(
46
+ latitude: result.dig("location", "lat"),
47
+ longitude: result.dig("location", "lng"),
48
+ timezone: result.dig("fields", "timezone", "name")
49
+ )
50
+ end
51
+ end
52
+ ```
53
+
54
+ ### 2. Associate with Models
55
+
56
+ Use the `has_operation` method in your models:
57
+
58
+ ```ruby
59
+ class Location < ApplicationRecord
60
+ has_operation :geocoding, class_name: "Geocoding::V1"
61
+ end
62
+ ```
63
+
64
+ ### 3. Save Operations
65
+
66
+ You are responsible for saving the associated operation record.
67
+
68
+ ```ruby
69
+ # Save an associated operation for a new record, within a transaction
70
+ location = Location.new(location_params)
71
+ location.build_geocoding
72
+ location.save
73
+
74
+ # Save an associated operation for an existing record
75
+ location = Location.find(params[:id])
76
+ location.geocoding.save
77
+ ```
78
+
79
+ ### 4. Perform Operations
80
+
81
+ ```ruby
82
+ # Synchronous execution
83
+ location = Location.find(params[:id])
84
+ location.geocoding.perform
85
+
86
+ # Asynchronous execution
87
+ location = Location.find(params[:id])
88
+ location.geocoding.perform_later
89
+ ```
90
+
91
+ ### 5. Check Operation Status
92
+
93
+ ```ruby
94
+ location.geocoding.received? # Operation completed request and stored response
95
+ location.geocoding.processed? # Operation completed processing of response
96
+ location.geocoding.errored? # Operation failed either request or process
97
+ ```
98
+
99
+ ## Development
100
+
101
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/test` to run the tests.
102
+
103
+ ## Contributing
104
+
105
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jeremysmithco/active_operator.
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveOperator
4
+ module Model
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ def has_operation(name, class_name: name.to_s.classify)
9
+ has_one name, class_name:, as: :record, inverse_of: :record, dependent: :destroy
10
+ define_method(name) { super() || public_send("build_#{name}") }
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveOperator
4
+ class Operation < ActiveRecord::Base
5
+ self.table_name = "active_operator_operations"
6
+
7
+ belongs_to :record, polymorphic: true
8
+
9
+ def received? = received_at?
10
+ def processed? = processed_at?
11
+ def errored? = errored_at?
12
+
13
+ def perform
14
+ request!
15
+ process!
16
+ rescue
17
+ errored!
18
+ raise
19
+ end
20
+
21
+ def perform_later
22
+ ActiveOperator::PerformOperationJob.perform_later(self)
23
+ end
24
+
25
+ def request!
26
+ return false if received?
27
+
28
+ update!(response: request, received_at: Time.current)
29
+ end
30
+
31
+ def process!
32
+ return false if !received?
33
+ return false if processed?
34
+
35
+ ActiveRecord::Base.transaction do
36
+ process
37
+ update!(processed_at: Time.current)
38
+ end
39
+ end
40
+
41
+ def errored!
42
+ update!(errored_at: Time.current)
43
+ end
44
+
45
+ def request
46
+ raise NotImplementedError, "Operations must implement the `request` method"
47
+ end
48
+
49
+ def process
50
+ raise NotImplementedError, "Operations must implement the `process` method"
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveOperator
4
+ class PerformOperationJob < ActiveJob::Base
5
+ discard_on ActiveRecord::RecordNotFound
6
+
7
+ def perform(operation)
8
+ operation.perform
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveOperator
4
+ class Railtie < Rails::Railtie
5
+ initializer "active_operator.include_model" do
6
+ ActiveSupport.on_load(:active_record) do
7
+ include ActiveOperator::Model
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveOperator
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_operator/version"
4
+ require "active_operator/model"
5
+ require "active_operator/operation"
6
+ require "active_operator/perform_operation_job"
7
+
8
+ module ActiveOperator
9
+ def self.table_name_prefix
10
+ "active_operator_"
11
+ end
12
+ end
13
+
14
+ require "active_operator/railtie" if defined?(Rails::Railtie)
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/active_record"
4
+
5
+ module ActiveOperator
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ include ActiveRecord::Generators::Migration
9
+
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ def copy_migration_file
13
+ migration_template(
14
+ "create_active_operator_operations.rb.erb",
15
+ "db/migrate/create_active_operator_operations.rb"
16
+ )
17
+ end
18
+
19
+ def create_application_operation
20
+ template(
21
+ "application_operation.rb.erb",
22
+ "app/operations/application_operation.rb"
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ class ApplicationOperation < ActiveOperator::Operation
2
+ self.abstract_class = true
3
+ end
@@ -0,0 +1,14 @@
1
+ class CreateActiveOperatorOperations < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :active_operator_operations do |t|
4
+ t.string :type, null: false
5
+ t.references :record, polymorphic: true, null: false, index: true
6
+ t.json :response, null: false, default: "{}"
7
+ t.datetime :received_at
8
+ t.datetime :processed_at
9
+ t.datetime :errored_at
10
+
11
+ t.timestamps
12
+ end
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_operator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jeremy Smith
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activejob
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.2'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.2'
26
+ - !ruby/object:Gem::Dependency
27
+ name: activerecord
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '7.2'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '7.2'
40
+ email:
41
+ - jeremy@jeremysmith.co
42
+ executables: []
43
+ extensions: []
44
+ extra_rdoc_files: []
45
+ files:
46
+ - CHANGELOG.md
47
+ - LICENSE
48
+ - README.md
49
+ - lib/active_operator.rb
50
+ - lib/active_operator/model.rb
51
+ - lib/active_operator/operation.rb
52
+ - lib/active_operator/perform_operation_job.rb
53
+ - lib/active_operator/railtie.rb
54
+ - lib/active_operator/version.rb
55
+ - lib/generators/active_operator/install_generator.rb
56
+ - lib/generators/active_operator/templates/application_operation.rb.erb
57
+ - lib/generators/active_operator/templates/create_active_operator_operations.rb.erb
58
+ homepage: https://github.com/jeremysmithco/active_operator
59
+ licenses:
60
+ - MIT
61
+ metadata:
62
+ homepage_uri: https://github.com/jeremysmithco/active_operator
63
+ source_code_uri: https://github.com/jeremysmithco/active_operator
64
+ changelog_uri: https://github.com/jeremysmithco/active_operator/blob/main/CHANGELOG.md
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubygems_version: 3.6.9
80
+ specification_version: 4
81
+ summary: A Rails pattern for calling external APIs, then storing and processing their
82
+ responses
83
+ test_files: []