actify 0.1.0 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b181ccd204525447ca9d58c74484f0c462281b2c06036d37a5e18895f39e5463
4
- data.tar.gz: 3114b05c900c3b0e37711d9bf8fd6b7f7c5e44afed997c452d69c5d4e902543e
3
+ metadata.gz: c2913716ba42355268f383ec908eb7b7cea22f1929512b636b2b183c9fd68181
4
+ data.tar.gz: 5ec5793bc6ef9eabafac9940ad991149ff07d1e1b883d91d48060cd17ae9a137
5
5
  SHA512:
6
- metadata.gz: 88e0e02c37af8bedadbe5f6ffa1298e9230f58d8ebcbf51f22907c78c901da136684ffa9ff1a11e3de8615a3b118b15c304e1b9f6c2f16f693d036b8b75517ad
7
- data.tar.gz: c4aa854bb3ed3749eed3ef8b0647cff2c5aa4f783542695ac0c64268b578c07861ea7267e13c9b43b2433ab504510564ab2bc8d4e2d50dfb729008804a0bf397
6
+ metadata.gz: 0dae8c31d8c79c6f1c4e2562bf09ee50284770cc38e8d23dc2b1dd5431ecc897ee55b5dd7d4c24b826df624dbf1e3f587f21db06411f56ccba004a23fa90ae05
7
+ data.tar.gz: b8233cefc428c863087171bc27f93625651987eb88e311e9f18f32c7758cf270783f0791d3fb0732ebbebd618a670201660b163c48afca2dbfb05f9c37230b5e
data/Gemfile.lock ADDED
@@ -0,0 +1,65 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ actify (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ ast (2.4.3)
10
+ diff-lcs (1.6.2)
11
+ json (2.12.2)
12
+ language_server-protocol (3.17.0.5)
13
+ lint_roller (1.1.0)
14
+ parallel (1.27.0)
15
+ parser (3.3.8.0)
16
+ ast (~> 2.4.1)
17
+ racc
18
+ prism (1.4.0)
19
+ racc (1.8.1)
20
+ rainbow (3.1.1)
21
+ rake (13.3.0)
22
+ regexp_parser (2.10.0)
23
+ rspec (3.13.1)
24
+ rspec-core (~> 3.13.0)
25
+ rspec-expectations (~> 3.13.0)
26
+ rspec-mocks (~> 3.13.0)
27
+ rspec-core (3.13.5)
28
+ rspec-support (~> 3.13.0)
29
+ rspec-expectations (3.13.5)
30
+ diff-lcs (>= 1.2.0, < 2.0)
31
+ rspec-support (~> 3.13.0)
32
+ rspec-mocks (3.13.5)
33
+ diff-lcs (>= 1.2.0, < 2.0)
34
+ rspec-support (~> 3.13.0)
35
+ rspec-support (3.13.4)
36
+ rubocop (1.77.0)
37
+ json (~> 2.3)
38
+ language_server-protocol (~> 3.17.0.2)
39
+ lint_roller (~> 1.1.0)
40
+ parallel (~> 1.10)
41
+ parser (>= 3.3.0.2)
42
+ rainbow (>= 2.2.2, < 4.0)
43
+ regexp_parser (>= 2.9.3, < 3.0)
44
+ rubocop-ast (>= 1.45.1, < 2.0)
45
+ ruby-progressbar (~> 1.7)
46
+ unicode-display_width (>= 2.4.0, < 4.0)
47
+ rubocop-ast (1.45.1)
48
+ parser (>= 3.3.7.2)
49
+ prism (~> 1.4)
50
+ ruby-progressbar (1.13.0)
51
+ unicode-display_width (3.1.4)
52
+ unicode-emoji (~> 4.0, >= 4.0.4)
53
+ unicode-emoji (4.0.4)
54
+
55
+ PLATFORMS
56
+ arm64-darwin-23
57
+
58
+ DEPENDENCIES
59
+ actify!
60
+ rake (~> 13.0)
61
+ rspec (~> 3.0)
62
+ rubocop (~> 1.21)
63
+
64
+ BUNDLED WITH
65
+ 2.3.27
@@ -0,0 +1,48 @@
1
+ require "rails/generators"
2
+ require "rails/generators/migration"
3
+
4
+ module Actify
5
+ module Generators
6
+ class InstallGenerator < Rails::Generators::Base
7
+ include Rails::Generators::Migration
8
+
9
+ source_root File.expand_path("../../../templates", __FILE__)
10
+ desc "Creates a migration for action_logs table"
11
+
12
+ def copy_migration
13
+ migration_template(
14
+ "create_action_logs.rb.tt",
15
+ "db/migrate/create_action_logs.rb",
16
+ migration_version: migration_version
17
+ )
18
+ end
19
+
20
+ def copy_model
21
+ template "action_log.rb.tt", "app/models/action_log.rb"
22
+ end
23
+
24
+ def run_migration
25
+ say_status("migrating", "Running `rails db:migrate`", :green)
26
+ rake("db:migrate")
27
+ end
28
+
29
+ def self.next_migration_number(dirname)
30
+ if @prev_migration_nr
31
+ @prev_migration_nr += 1
32
+ else
33
+ timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S")
34
+ @prev_migration_nr = timestamp.to_i
35
+ end
36
+
37
+ @prev_migration_nr.to_s
38
+ end
39
+
40
+ private
41
+
42
+ # Detect version from Rails project
43
+ def migration_version
44
+ "[#{ActiveRecord::Migration.current_version.to_s[0..2]}]"
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+ require "rails/generators"
5
+
6
+ module Actify
7
+ # Railtie to integrate Actify with Rails
8
+ class Railtie < Rails::Railtie
9
+ # Load custom Rails commands
10
+ railtie_name :actify
11
+
12
+ generators do
13
+ require File.expand_path("../generators/actify/install_generator", __FILE__)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,6 @@
1
+ class ActionLog < ApplicationRecord
2
+ belongs_to :actor, class_name: "User"
3
+ belongs_to :actionable, polymorphic: true
4
+
5
+ enum status: %i[created aborted finished failed]
6
+ end
@@ -0,0 +1,21 @@
1
+ class CreateActionLogs < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :action_logs do |t|
4
+ t.string :status
5
+ t.string :actor_id
6
+ t.string :action_code
7
+ t.string :actionable_type
8
+ t.string :actionable_id
9
+ t.string :action_label
10
+ t.text :action_data
11
+ t.text :context
12
+ t.text :object_before
13
+ t.text :object_after
14
+
15
+ t.timestamps
16
+ end
17
+
18
+ add_index :action_logs, [:actionable_type, :actionable_id]
19
+ add_index :action_logs, :actor_id
20
+ end
21
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Actify
4
- VERSION = "0.1.0"
4
+ VERSION = "1.0.0"
5
5
  end
data/lib/actify.rb CHANGED
@@ -1,11 +1,159 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "actify/railtie" if defined?(Rails)
3
4
  require_relative "actify/version"
5
+ require_relative "action"
4
6
 
7
+ # This module allows models to define and execute custom actions with various hooks,
8
+ # authorization checks, and lifecycle callbacks.
5
9
  module Actify
6
- class Error < StandardError; end
10
+ def self.included(base)
11
+ return if base.const_defined?(:Actions)
7
12
 
8
- def self.perform(action)
9
- "Performing: #{action}"
13
+ base.const_set :Actions, (Module.new do
14
+ def self.all
15
+ @actions
16
+ end
17
+
18
+ def self.[](code)
19
+ (@actions || {})[code]
20
+ end
21
+ end)
22
+
23
+ class << base
24
+ def use_policy_in_actionable(accept = true) # rubocop:disable Style/OptionalBooleanParameter
25
+ @use_policy = accept
26
+ end
27
+
28
+ def action(name, options = {}, &block)
29
+ options["use_policy"] = @use_policy if (options.keys & [:use_policy, "use_policy"]).empty?
30
+ action_def = Def.new(name, options)
31
+
32
+ existing_action = const_get(:Actions).class_eval do
33
+ (@actions || {})[name]
34
+ end
35
+
36
+ if existing_action
37
+ action_def.action = existing_action
38
+ action_def.with_options(options)
39
+ end
40
+
41
+ action_def.instance_eval(&block) if block_given?
42
+
43
+ const_get(:Actions).class_eval do
44
+ @actions ||= {}
45
+ @actions[name] = action_def.action
46
+ end
47
+ end
48
+ end
49
+
50
+ base.action :create, label: "Create" do
51
+ show? { false }
52
+ commit do |object, ctx|
53
+ end
54
+ end
55
+
56
+ base.action :update, label: "Update" do
57
+ show? { false }
58
+ commit do |object, ctx|
59
+ end
60
+ end
61
+ end
62
+
63
+ # Context class to hold action context data
64
+ # It is a Hash-like object that provides access to common attributes like actor, data
65
+ class Context < Hash
66
+ %i[actor data].each do |attr_name|
67
+ define_method attr_name do
68
+ self[attr_name]
69
+ end
70
+
71
+ define_method "#{attr_name}=".to_sym do |value|
72
+ self[attr_name] = value
73
+ end
74
+ end
75
+
76
+ def initialize(options = {}) # rubocop:disable Lint/MissingSuper
77
+ # TODO: check option validity
78
+ options.each do |key, value|
79
+ self[key] = value
80
+ end
81
+ end
82
+ end
83
+
84
+ class Def # rubocop:disable Style/Documentation
85
+ attr_accessor :action
86
+
87
+ def initialize(name, options = {})
88
+ @action = Action.new
89
+ @action.code = name
90
+
91
+ with_options options
92
+ end
93
+
94
+ def with_options(options)
95
+ options.each do |key, value|
96
+ send(key.to_s, value)
97
+ end
98
+ end
99
+
100
+ def label(label)
101
+ @action.label = label
102
+ end
103
+
104
+ def order(order)
105
+ @action.order = order
106
+ end
107
+
108
+ def type(type)
109
+ @action.type = type
110
+ end
111
+
112
+ def use_policy(use_policy)
113
+ @action.use_policy = use_policy
114
+ end
115
+
116
+ def execute_before_action(action_code, options = {})
117
+ @action.before_actions << {
118
+ action_code: action_code,
119
+ options: options
120
+ }
121
+ end
122
+
123
+ def execute_after_action(action_code, options = {})
124
+ @action.after_actions << {
125
+ action_code: action_code,
126
+ options: options
127
+ }
128
+ end
129
+
130
+ def show?(&block)
131
+ @action.hdl_show = block
132
+ end
133
+
134
+ def authorized?(&block)
135
+ @action.hdl_authorized = block
136
+ end
137
+
138
+ def commitable?(&block)
139
+ @action.hdl_commitable = block
140
+ end
141
+
142
+ def commit(&block)
143
+ @action.hdl_commit = block
144
+ end
145
+
146
+ def finalize(&block)
147
+ @action.hdl_finalize = block
148
+ end
149
+ end
150
+
151
+ class Error < StandardError
152
+ end
153
+
154
+ class ActorMissingError < Error
155
+ end
156
+
157
+ class InvalidDataError < Error
10
158
  end
11
159
  end
data/lib/action.rb ADDED
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Action # rubocop:disable Metrics/ClassLength,Style/Documentation
4
+ attr_accessor :code, :label, :order, :use_policy, :after_actions, :before_actions, :hdl_show, :hdl_authorized,
5
+ :hdl_commitable, :hdl_commit, :hdl_finalize
6
+
7
+ def initialize
8
+ @order = 0
9
+ @after_actions = []
10
+ @before_actions = []
11
+ @error = nil
12
+ end
13
+
14
+ def commit!(object, context)
15
+ execute(object, context)
16
+ end
17
+
18
+ def show?(object, context)
19
+ if use_policy
20
+ if hdl_show
21
+ instance_exec(object, context, &hdl_show)
22
+ else
23
+ authorized?(object, context) && commitable?(object, context)
24
+ end
25
+ else
26
+ !hdl_show || instance_exec(object, context, &hdl_show)
27
+ end
28
+ end
29
+
30
+ def authorized?(object, context) # rubocop:disable Metrics/MethodLength
31
+ # if use_policy
32
+ # if hdl_authorized
33
+ # instance_exec(object, context, &hdl_authorized)
34
+ # else
35
+ # context_for_policy = context.merge({ user: context[:actor] })
36
+ # policy = Pundit.policy!(context_for_policy, object)
37
+ # policy.send("#{code}?")
38
+ # end
39
+ # else
40
+ # !hdl_authorized || instance_exec(object, context, &hdl_authorized)
41
+ # end
42
+ !hdl_authorized || instance_exec(object, context, &hdl_authorized)
43
+ end
44
+
45
+ def commitable?(object, context)
46
+ !hdl_commitable || instance_exec(object, context, &hdl_commitable)
47
+ end
48
+
49
+ def after_action?(action_code)
50
+ !!after_actions.detect { |after_action| after_action[:action_code] == action_code }
51
+ end
52
+
53
+ def before_action?(action_code)
54
+ !!before_actions.detect { |before_action| before_action[:action_code] == action_code }
55
+ end
56
+
57
+ private
58
+
59
+ def commit(object, context)
60
+ instance_exec object, context, &hdl_commit
61
+ end
62
+
63
+ def verify_context(context)
64
+ return if context.actor
65
+
66
+ raise Actify::ActorMissingError, "Actor is required to commit action"
67
+ end
68
+
69
+ def execute(object, context) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
70
+ verify_context(context)
71
+
72
+ log = initialize_action_log(object, context)
73
+
74
+ if !(commitable = authorized?(object, context))
75
+ log.status = ActionLog.statuses[:aborted]
76
+ log.error = { message: "Unauthorized" }
77
+ elsif !(commmitable = commitable && commitable?(object, context))
78
+ log.status = ActionLog.statuses[:aborted]
79
+ log.error = { message: "Wrong context" }
80
+ end
81
+
82
+ if commmitable
83
+ begin
84
+ execute_before_actions object, context
85
+
86
+ commit object, context
87
+
88
+ execute_after_actions object, context
89
+
90
+ log.status = ActionLog.statuses[:finished]
91
+ rescue StandardError => e
92
+ log.status = ActionLog.statuses[:aborted]
93
+ log.error = { message: e.message }
94
+ end
95
+ end
96
+
97
+ finalize_action_log log, object, context
98
+
99
+ log
100
+ end
101
+
102
+ def finalize_action_log(log, object, context)
103
+ object_before = {}
104
+ object_after = {}
105
+
106
+ object.previous_changes.each do |k, v|
107
+ object_before[k] = v[0]
108
+ object_after[k] = v[1]
109
+ end
110
+
111
+ log.object_before = object_before
112
+ log.object_after = object_after
113
+
114
+ instance_exec(log, object, context, &hdl_finalize) if hdl_finalize
115
+
116
+ log.save!
117
+ end
118
+
119
+ def execute_before_actions(object, context)
120
+ to_run_actions = object.class::Actions.all.select do |_code, action|
121
+ action.before_action? code
122
+ end
123
+
124
+ to_run_actions.each_value do |action|
125
+ # if action.executable?(context, object, params)
126
+ action.commit! object, context
127
+ # end
128
+ end
129
+ end
130
+
131
+ def execute_after_actions(object, context)
132
+ to_run_actions = object.class::Actions.all.select do |_code, action|
133
+ action.after_action? code
134
+ end
135
+
136
+ to_run_actions.each_value do |action|
137
+ # if action.executable?(context, object, params)
138
+ action.commit! object, context
139
+ # end
140
+ end
141
+ end
142
+
143
+ def initialize_action_log(object, context) # rubocop:disable Metrics/MethodLength
144
+ ActionLog.create!(
145
+ status: ActionLog.statuses[:created],
146
+ actor_id: context.actor.id,
147
+ actionable: object,
148
+ action_code: code,
149
+ action_label: label,
150
+ action_data: context.data.to_s,
151
+ context: context.to_s,
152
+ object_before: object.to_s
153
+ )
154
+ end
155
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actify
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - nguyenta99
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-07-03 00:00:00.000000000 Z
11
+ date: 2025-07-05 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -22,11 +22,17 @@ files:
22
22
  - CHANGELOG.md
23
23
  - CODE_OF_CONDUCT.md
24
24
  - Gemfile
25
+ - Gemfile.lock
25
26
  - LICENSE.txt
26
27
  - README.md
27
28
  - Rakefile
28
29
  - lib/actify.rb
30
+ - lib/actify/generators/actify/install_generator.rb
31
+ - lib/actify/railtie.rb
32
+ - lib/actify/templates/action_log.rb.tt
33
+ - lib/actify/templates/create_action_logs.rb.tt
29
34
  - lib/actify/version.rb
35
+ - lib/action.rb
30
36
  - sig/actify.rbs
31
37
  homepage: https://nguyenta99.com
32
38
  licenses: