decision_agent 0.1.1 → 0.1.2

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.
@@ -12,7 +12,7 @@ module DecisionAgent
12
12
  # Enable CORS for API calls
13
13
  before do
14
14
  headers["Access-Control-Allow-Origin"] = "*"
15
- headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
15
+ headers["Access-Control-Allow-Methods"] = "GET, POST, DELETE, OPTIONS"
16
16
  headers["Access-Control-Allow-Headers"] = "Content-Type"
17
17
  end
18
18
 
@@ -223,8 +223,173 @@ module DecisionAgent
223
223
  { status: "ok", version: DecisionAgent::VERSION }.to_json
224
224
  end
225
225
 
226
+ # Versioning API endpoints
227
+
228
+ # Create a new version
229
+ post "/api/versions" do
230
+ content_type :json
231
+
232
+ begin
233
+ request_body = request.body.read
234
+ data = JSON.parse(request_body)
235
+
236
+ rule_id = data["rule_id"]
237
+ rule_content = data["content"]
238
+ created_by = data["created_by"] || "system"
239
+ changelog = data["changelog"]
240
+
241
+ version = version_manager.save_version(
242
+ rule_id: rule_id,
243
+ rule_content: rule_content,
244
+ created_by: created_by,
245
+ changelog: changelog
246
+ )
247
+
248
+ status 201
249
+ version.to_json
250
+
251
+ rescue => e
252
+ status 500
253
+ { error: e.message }.to_json
254
+ end
255
+ end
256
+
257
+ # List all versions for a rule
258
+ get "/api/rules/:rule_id/versions" do
259
+ content_type :json
260
+
261
+ begin
262
+ rule_id = params[:rule_id]
263
+ limit = params[:limit]&.to_i
264
+
265
+ versions = version_manager.get_versions(rule_id: rule_id, limit: limit)
266
+
267
+ versions.to_json
268
+
269
+ rescue => e
270
+ status 500
271
+ { error: e.message }.to_json
272
+ end
273
+ end
274
+
275
+ # Get version history with metadata
276
+ get "/api/rules/:rule_id/history" do
277
+ content_type :json
278
+
279
+ begin
280
+ rule_id = params[:rule_id]
281
+ history = version_manager.get_history(rule_id: rule_id)
282
+
283
+ history.to_json
284
+
285
+ rescue => e
286
+ status 500
287
+ { error: e.message }.to_json
288
+ end
289
+ end
290
+
291
+ # Get a specific version
292
+ get "/api/versions/:version_id" do
293
+ content_type :json
294
+
295
+ begin
296
+ version_id = params[:version_id]
297
+ version = version_manager.get_version(version_id: version_id)
298
+
299
+ if version
300
+ version.to_json
301
+ else
302
+ status 404
303
+ { error: "Version not found" }.to_json
304
+ end
305
+
306
+ rescue => e
307
+ status 500
308
+ { error: e.message }.to_json
309
+ end
310
+ end
311
+
312
+ # Activate a version (rollback)
313
+ post "/api/versions/:version_id/activate" do
314
+ content_type :json
315
+
316
+ begin
317
+ version_id = params[:version_id]
318
+ request_body = request.body.read
319
+ data = request_body.empty? ? {} : JSON.parse(request_body)
320
+ performed_by = data["performed_by"] || "system"
321
+
322
+ version = version_manager.rollback(
323
+ version_id: version_id,
324
+ performed_by: performed_by
325
+ )
326
+
327
+ version.to_json
328
+
329
+ rescue => e
330
+ status 500
331
+ { error: e.message }.to_json
332
+ end
333
+ end
334
+
335
+ # Compare two versions
336
+ get "/api/versions/:version_id_1/compare/:version_id_2" do
337
+ content_type :json
338
+
339
+ begin
340
+ version_id_1 = params[:version_id_1]
341
+ version_id_2 = params[:version_id_2]
342
+
343
+ comparison = version_manager.compare(
344
+ version_id_1: version_id_1,
345
+ version_id_2: version_id_2
346
+ )
347
+
348
+ if comparison
349
+ comparison.to_json
350
+ else
351
+ status 404
352
+ { error: "One or both versions not found" }.to_json
353
+ end
354
+
355
+ rescue => e
356
+ status 500
357
+ { error: e.message }.to_json
358
+ end
359
+ end
360
+
361
+ # Delete a version
362
+ delete "/api/versions/:version_id" do
363
+ content_type :json
364
+
365
+ begin
366
+ version_id = params[:version_id]
367
+
368
+ version_manager.delete_version(version_id: version_id)
369
+
370
+ status 200
371
+ { success: true, message: "Version deleted successfully" }.to_json
372
+
373
+ rescue DecisionAgent::NotFoundError => e
374
+ status 404
375
+ { error: e.message }.to_json
376
+
377
+ rescue DecisionAgent::ValidationError => e
378
+ status 422
379
+ { error: e.message }.to_json
380
+
381
+ rescue => e
382
+ status 500
383
+ { error: e.message }.to_json
384
+ end
385
+ end
386
+
226
387
  private
227
388
 
389
+ def version_manager
390
+ @version_manager ||= DecisionAgent::Versioning::VersionManager.new
391
+ end
392
+
228
393
  def parse_validation_errors(error_message)
229
394
  # Extract individual errors from the formatted error message
230
395
  errors = []
@@ -25,5 +25,9 @@ require_relative "decision_agent/audit/logger_adapter"
25
25
 
26
26
  require_relative "decision_agent/replay/replay"
27
27
 
28
+ require_relative "decision_agent/versioning/adapter"
29
+ require_relative "decision_agent/versioning/file_storage_adapter"
30
+ require_relative "decision_agent/versioning/version_manager"
31
+
28
32
  module DecisionAgent
29
33
  end
@@ -0,0 +1,40 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+
4
+ module DecisionAgent
5
+ module Generators
6
+ class InstallGenerator < Rails::Generators::Base
7
+ include Rails::Generators::Migration
8
+
9
+ source_root File.expand_path('templates', __dir__)
10
+
11
+ desc "Installs DecisionAgent models and migrations for Rails"
12
+
13
+ def self.next_migration_number(dirname)
14
+ next_migration_number = current_migration_number(dirname) + 1
15
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
16
+ end
17
+
18
+ def copy_migration
19
+ migration_template "migration.rb",
20
+ "db/migrate/create_decision_agent_tables.rb",
21
+ migration_version: migration_version
22
+ end
23
+
24
+ def copy_models
25
+ copy_file "rule.rb", "app/models/rule.rb"
26
+ copy_file "rule_version.rb", "app/models/rule_version.rb"
27
+ end
28
+
29
+ def show_readme
30
+ readme "README" if behavior == :invoke
31
+ end
32
+
33
+ private
34
+
35
+ def migration_version
36
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,47 @@
1
+ ===============================================================================
2
+
3
+ DecisionAgent has been installed!
4
+
5
+ Next steps:
6
+
7
+ 1. Run the migrations:
8
+
9
+ rails db:migrate
10
+
11
+ 2. The following models have been created:
12
+ - Rule (app/models/rule.rb)
13
+ - RuleVersion (app/models/rule_version.rb)
14
+
15
+ 3. Start using the versioning system:
16
+
17
+ # Create a rule with a version
18
+ rule = Rule.create!(
19
+ rule_id: 'approval_rule_001',
20
+ ruleset: 'approval',
21
+ description: 'Approval decision rules'
22
+ )
23
+
24
+ # Create a version
25
+ rule.create_version(
26
+ content: { /* your rule JSON */ },
27
+ created_by: 'admin',
28
+ changelog: 'Initial version'
29
+ )
30
+
31
+ # Or use the VersionManager directly
32
+ manager = DecisionAgent::Versioning::VersionManager.new
33
+ manager.save_version(
34
+ rule_id: 'approval_rule_001',
35
+ rule_content: { /* your rule JSON */ },
36
+ created_by: 'admin'
37
+ )
38
+
39
+ 4. Optional: Mount the DecisionAgent routes in config/routes.rb:
40
+
41
+ # This is planned for a future release
42
+ # mount DecisionAgent::Engine => '/decision_agent'
43
+
44
+ For more information, visit:
45
+ https://github.com/samaswin87/decision_agent
46
+
47
+ ===============================================================================
@@ -0,0 +1,26 @@
1
+ class CreateDecisionAgentTables < ActiveRecord::Migration[7.0]
2
+ def change
3
+ # Rules table
4
+ create_table :rules do |t|
5
+ t.string :rule_id, null: false, index: { unique: true }
6
+ t.string :ruleset, null: false
7
+ t.text :description
8
+ t.string :status, default: 'active'
9
+ t.timestamps
10
+ end
11
+
12
+ # Rule versions table
13
+ create_table :rule_versions do |t|
14
+ t.string :rule_id, null: false, index: true
15
+ t.integer :version_number, null: false
16
+ t.text :content, null: false # JSON rule definition
17
+ t.string :created_by, null: false, default: 'system'
18
+ t.text :changelog
19
+ t.string :status, null: false, default: 'draft' # draft, active, archived
20
+ t.timestamps
21
+ end
22
+
23
+ add_index :rule_versions, [:rule_id, :version_number], unique: true
24
+ add_index :rule_versions, [:rule_id, :status]
25
+ end
26
+ end
@@ -0,0 +1,30 @@
1
+ class Rule < ApplicationRecord
2
+ has_many :rule_versions, primary_key: :rule_id, foreign_key: :rule_id, dependent: :destroy
3
+
4
+ validates :rule_id, presence: true, uniqueness: true
5
+ validates :ruleset, presence: true
6
+ validates :status, inclusion: { in: %w[active inactive archived] }
7
+
8
+ scope :active, -> { where(status: 'active') }
9
+ scope :by_ruleset, ->(ruleset) { where(ruleset: ruleset) }
10
+
11
+ # Get the active version for this rule
12
+ def active_version
13
+ rule_versions.find_by(status: 'active')
14
+ end
15
+
16
+ # Get all versions ordered by version number
17
+ def versions
18
+ rule_versions.order(version_number: :desc)
19
+ end
20
+
21
+ # Create a new version
22
+ def create_version(content:, created_by: 'system', changelog: nil)
23
+ DecisionAgent::Versioning::VersionManager.new.save_version(
24
+ rule_id: rule_id,
25
+ rule_content: content,
26
+ created_by: created_by,
27
+ changelog: changelog
28
+ )
29
+ end
30
+ end
@@ -0,0 +1,60 @@
1
+ class RuleVersion < ApplicationRecord
2
+ belongs_to :rule, primary_key: :rule_id, foreign_key: :rule_id, optional: true
3
+
4
+ validates :rule_id, presence: true
5
+ validates :version_number, presence: true, uniqueness: { scope: :rule_id }
6
+ validates :content, presence: true
7
+ validates :status, inclusion: { in: %w[draft active archived] }
8
+ validates :created_by, presence: true
9
+
10
+ scope :active, -> { where(status: 'active') }
11
+ scope :for_rule, ->(rule_id) { where(rule_id: rule_id).order(version_number: :desc) }
12
+ scope :latest, -> { order(version_number: :desc).limit(1) }
13
+
14
+ before_create :set_next_version_number
15
+
16
+ # Parse the JSON content
17
+ def parsed_content
18
+ JSON.parse(content, symbolize_names: true)
19
+ rescue JSON::ParserError
20
+ {}
21
+ end
22
+
23
+ # Set content from a hash
24
+ def content_hash=(hash)
25
+ self.content = hash.to_json
26
+ end
27
+
28
+ # Activate this version (deactivates others)
29
+ def activate!
30
+ transaction do
31
+ # Deactivate all other versions for this rule
32
+ self.class.where(rule_id: rule_id, status: 'active')
33
+ .where.not(id: id)
34
+ .update_all(status: 'archived')
35
+
36
+ # Activate this version
37
+ update!(status: 'active')
38
+ end
39
+ end
40
+
41
+ # Compare with another version
42
+ def compare_with(other_version)
43
+ DecisionAgent::Versioning::VersionManager.new.compare(
44
+ version_id_1: id,
45
+ version_id_2: other_version.id
46
+ )
47
+ end
48
+
49
+ private
50
+
51
+ def set_next_version_number
52
+ return if version_number.present?
53
+
54
+ last_version = self.class.where(rule_id: rule_id)
55
+ .order(version_number: :desc)
56
+ .first
57
+
58
+ self.version_number = last_version ? last_version.version_number + 1 : 1
59
+ end
60
+ end