event_system 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.
data/exe/event_system ADDED
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/event_system'
5
+ require 'optparse'
6
+
7
+ class EventSystemCLI
8
+ def initialize
9
+ @options = {}
10
+ @parser = OptionParser.new do |opts|
11
+ opts.banner = "Usage: event_system [options] command"
12
+
13
+ opts.on("-s", "--storage TYPE", "Storage type (memory, file)") do |type|
14
+ @options[:storage_type] = type.to_sym
15
+ end
16
+
17
+ opts.on("-d", "--directory DIR", "Directory for file storage") do |dir|
18
+ @options[:storage_options] ||= {}
19
+ @options[:storage_options][:directory] = dir
20
+ end
21
+
22
+ opts.on("-v", "--verbose", "Verbose output") do
23
+ @options[:verbose] = true
24
+ end
25
+
26
+ opts.on("-h", "--help", "Show this help message") do
27
+ puts opts
28
+ exit
29
+ end
30
+ end
31
+ end
32
+
33
+ def run(args)
34
+ @parser.parse!(args)
35
+
36
+ command = args.shift
37
+ case command
38
+ when "visualize"
39
+ visualize_command(args)
40
+ when "stats"
41
+ stats_command(args)
42
+ when "list"
43
+ list_command(args)
44
+ when "query"
45
+ query_command(args)
46
+ else
47
+ puts "Unknown command: #{command}"
48
+ puts "Available commands: visualize, stats, list, query"
49
+ exit 1
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def create_manager
56
+ config = {
57
+ storage_type: @options[:storage_type] || :file,
58
+ storage_options: @options[:storage_options] || { directory: "event_logs" }
59
+ }
60
+
61
+ EventSystem.create_manager(config)
62
+ end
63
+
64
+ def visualize_command(args)
65
+ manager = create_manager
66
+ session_id = args[0]
67
+ output_file = args[1]
68
+
69
+ generator = EventSystem::Visualization::TimelineGenerator.new(manager.storage)
70
+ output_path = generator.generate_timeline(session_id, output_file)
71
+
72
+ if output_path
73
+ puts "Timeline generated: #{output_path}"
74
+ else
75
+ puts "No events found for visualization"
76
+ exit 1
77
+ end
78
+ end
79
+
80
+ def stats_command(args)
81
+ manager = create_manager
82
+ stats = manager.stats
83
+
84
+ puts "Event System Statistics:"
85
+ puts "========================"
86
+ puts "Storage: #{stats[:storage][:type]}"
87
+ puts "Sessions: #{stats[:storage][:sessions]}"
88
+ puts "Subscribers: #{stats[:subscribers].values.sum}"
89
+ puts
90
+ puts "Event types:"
91
+ stats[:subscribers].each do |type, count|
92
+ puts " #{type}: #{count} subscribers"
93
+ end
94
+ end
95
+
96
+ def list_command(args)
97
+ manager = create_manager
98
+ sessions = manager.storage.list_sessions
99
+
100
+ puts "Available Sessions:"
101
+ puts "==================="
102
+ if sessions.empty?
103
+ puts "No sessions found"
104
+ else
105
+ sessions.each do |session|
106
+ events = manager.storage.load_session(session)
107
+ puts "#{session} (#{events.length} events)"
108
+ end
109
+ end
110
+ end
111
+
112
+ def query_command(args)
113
+ manager = create_manager
114
+ session_id = args[0]
115
+ event_type = args[1]
116
+
117
+ options = {}
118
+ options[:session_id] = session_id if session_id
119
+ options[:type] = event_type if event_type
120
+
121
+ events = manager.query_events(options)
122
+
123
+ puts "Query Results (#{events.length} events):"
124
+ puts "======================================"
125
+ events.each do |event|
126
+ puts "[#{event.timestamp}] #{event.type} from #{event.source_to_string}"
127
+ if @options[:verbose]
128
+ puts " Data: #{event.data}"
129
+ puts " ID: #{event.id}"
130
+ puts
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ if __FILE__ == $0
137
+ cli = EventSystemCLI.new
138
+ cli.run(ARGV)
139
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventSystem
4
+ # Configuration class for the EventSystem
5
+ # Provides a centralized way to configure storage, logging, and other options
6
+ class Configuration
7
+ attr_accessor :storage_type, :storage_options, :logger, :session_id, :auto_flush
8
+
9
+ # Initialize a new configuration
10
+ def initialize
11
+ @storage_type = :memory
12
+ @storage_options = {}
13
+ @logger = nil
14
+ @session_id = nil
15
+ @auto_flush = true
16
+ end
17
+
18
+ # Get the storage class based on the configured type
19
+ # @return [Class] The storage class to use
20
+ def storage_class
21
+ case @storage_type
22
+ when :memory
23
+ Storage::MemoryStore
24
+ when :file
25
+ Storage::FileStore
26
+ else
27
+ raise ArgumentError, "Unknown storage type: #{@storage_type}. Available types: :memory, :file"
28
+ end
29
+ end
30
+
31
+ # Create a new storage instance with the current configuration
32
+ # @return [EventSystem::Storage::Base] A new storage instance
33
+ def create_storage
34
+ storage_class.new(*storage_constructor_args)
35
+ end
36
+
37
+ # Get the logger instance, creating a default one if none is set
38
+ # @return [Logger] The logger instance
39
+ def logger
40
+ @logger ||= create_default_logger
41
+ end
42
+
43
+ # Set the logger
44
+ # @param logger [Logger] The logger to use
45
+ def logger=(logger)
46
+ @logger = logger
47
+ end
48
+
49
+ # Get the current session ID, creating one if none is set
50
+ # @return [String] The current session ID
51
+ def session_id
52
+ @session_id ||= Time.now.strftime("%Y%m%d_%H%M%S")
53
+ end
54
+
55
+ # Set the session ID
56
+ # @param session_id [String] The session ID to use
57
+ def session_id=(session_id)
58
+ @session_id = session_id
59
+ end
60
+
61
+ # Reset the configuration to defaults
62
+ # @return [void]
63
+ def reset!
64
+ @storage_type = :memory
65
+ @storage_options = {}
66
+ @logger = nil
67
+ @session_id = nil
68
+ @auto_flush = true
69
+ end
70
+
71
+ # Validate the current configuration
72
+ # @return [Boolean] True if configuration is valid
73
+ def valid?
74
+ return false unless [:memory, :file].include?(@storage_type)
75
+ return false if @storage_options.nil?
76
+
77
+ case @storage_type
78
+ when :file
79
+ directory = @storage_options[:directory] || @storage_options['directory']
80
+ return false if directory.nil? || directory.empty?
81
+ end
82
+
83
+ true
84
+ end
85
+
86
+ # Get configuration as a hash
87
+ # @return [Hash] Configuration as a hash
88
+ def to_h
89
+ {
90
+ storage_type: @storage_type,
91
+ storage_options: @storage_options,
92
+ session_id: @session_id,
93
+ auto_flush: @auto_flush
94
+ }
95
+ end
96
+
97
+ private
98
+
99
+ # Get arguments for storage constructor
100
+ # @return [Array] Arguments for the storage constructor
101
+ def storage_constructor_args
102
+ case @storage_type
103
+ when :memory
104
+ [@session_id]
105
+ when :file
106
+ directory = @storage_options[:directory] || @storage_options['directory'] || "event_logs"
107
+ [directory, @session_id]
108
+ else
109
+ []
110
+ end
111
+ end
112
+
113
+ # Create a default logger
114
+ # @return [Logger] A default logger instance
115
+ def create_default_logger
116
+ require 'logger'
117
+ logger = Logger.new($stdout)
118
+ logger.level = Logger::INFO
119
+ logger.formatter = proc do |severity, datetime, progname, msg|
120
+ "[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}: #{msg}\n"
121
+ end
122
+ logger
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'time'
5
+ require 'securerandom'
6
+
7
+ module EventSystem
8
+ # Base class for all events in the system
9
+ # Provides unique ID generation, timestamp handling, and serialization
10
+ class Event
11
+ attr_reader :id, :timestamp, :source, :type, :data
12
+
13
+ # Initialize a new event
14
+ # @param type [String, Symbol] The event type identifier
15
+ # @param source [Object] The source/originator of the event
16
+ # @param data [Hash] Additional event data
17
+ # @param id [String, nil] Optional event ID, generated if not provided
18
+ # @param timestamp [Time, String, nil] Optional timestamp, current time if not provided
19
+ def initialize(type, source = nil, data = {}, id = nil, timestamp = nil)
20
+ @id = id || SecureRandom.uuid
21
+ @type = type.to_s
22
+ @source = source
23
+ @data = data
24
+ @timestamp = parse_timestamp(timestamp)
25
+ end
26
+
27
+ # String representation of the event
28
+ # @return [String] Human-readable event string
29
+ def to_s
30
+ "[#{@timestamp}] #{@type}: #{@data.inspect}"
31
+ end
32
+
33
+ # Hash representation of the event
34
+ # @return [Hash] Event data as a hash
35
+ def to_h
36
+ {
37
+ id: @id,
38
+ type: @type,
39
+ source: source_to_string,
40
+ timestamp: @timestamp.iso8601(3),
41
+ data: safe_serialize(@data)
42
+ }
43
+ end
44
+
45
+ # JSON representation of the event
46
+ # @return [String] Event data as a JSON string
47
+ def to_json(*_args)
48
+ to_h.to_json
49
+ end
50
+
51
+ # Create an event from its JSON representation
52
+ # @param json [String] JSON representation of an event
53
+ # @return [Event] Reconstructed event
54
+ def self.from_json(json)
55
+ data = JSON.parse(json, symbolize_names: true)
56
+
57
+ new(
58
+ data[:type],
59
+ data[:source],
60
+ data[:data] || {},
61
+ data[:id],
62
+ data[:timestamp]
63
+ )
64
+ end
65
+
66
+ # Check if two events are equal (by ID)
67
+ # @param other [Event] The other event to compare
68
+ # @return [Boolean] True if events have the same ID
69
+ def ==(other)
70
+ other.is_a?(Event) && @id == other.id
71
+ end
72
+
73
+ # Hash code for the event (based on ID)
74
+ # @return [Integer] Hash code
75
+ def hash
76
+ @id.hash
77
+ end
78
+
79
+ # Check if the event is of a specific type
80
+ # @param event_type [String, Symbol] The event type to check
81
+ # @return [Boolean] True if the event matches the type
82
+ def type?(event_type)
83
+ @type == event_type.to_s
84
+ end
85
+
86
+ # Convert source to string representation
87
+ # @return [String] String representation of the source
88
+ def source_to_string
89
+ case @source
90
+ when nil
91
+ 'unknown'
92
+ when String
93
+ @source
94
+ else
95
+ @source.to_s
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ # Parse timestamp from various formats
102
+ # @param timestamp [Time, String, nil] The timestamp to parse
103
+ # @return [Time] Parsed timestamp
104
+ def parse_timestamp(timestamp)
105
+ case timestamp
106
+ when Time
107
+ timestamp.utc
108
+ when String
109
+ Time.parse(timestamp).utc
110
+ when nil
111
+ Time.now.utc
112
+ else
113
+ raise ArgumentError, "Invalid timestamp type: #{timestamp.class}"
114
+ end
115
+ end
116
+
117
+ # Safely serialize data by handling non-serializable objects
118
+ # @param value [Object] The value to serialize
119
+ # @return [Object] A serializable version of the value
120
+ def safe_serialize(value)
121
+ case value
122
+ when Hash
123
+ value.each_with_object({}) do |(k, v), h|
124
+ h[k] = safe_serialize(v)
125
+ end
126
+ when Array
127
+ value.map { |v| safe_serialize(v) }
128
+ when Numeric, String, true, false, nil
129
+ value
130
+ else
131
+ # For complex objects, convert to string representation
132
+ value.to_s
133
+ end
134
+ rescue
135
+ # If any error occurs during serialization, return a safe fallback
136
+ "#<#{value.class} - non-serializable>"
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'event'
4
+ require_relative 'event_subscriber'
5
+ require_relative 'configuration'
6
+ require_relative 'storage/base'
7
+ require_relative 'storage/memory_store'
8
+ require_relative 'storage/file_store'
9
+
10
+ module EventSystem
11
+ # Central event management class that handles event publication and subscription
12
+ # Provides a complete event-driven architecture with pluggable storage backends
13
+ class EventManager
14
+ attr_reader :config, :storage
15
+
16
+ # Initialize a new event manager
17
+ # @param config [EventSystem::Configuration, Hash, nil] Configuration object, hash, or nil for defaults
18
+ def initialize(config = nil)
19
+ @config = case config
20
+ when Configuration
21
+ config
22
+ when Hash
23
+ Configuration.new.tap do |c|
24
+ c.storage_type = config[:storage_type] || :memory
25
+ c.storage_options = config[:storage_options] || {}
26
+ c.logger = config[:logger]
27
+ c.session_id = config[:session_id]
28
+ c.auto_flush = config[:auto_flush] != false
29
+ end
30
+ else
31
+ Configuration.new
32
+ end
33
+
34
+ @subscribers = Hash.new { |h, k| h[k] = [] }
35
+ @storage = @config.create_storage
36
+ @logger = @config.logger
37
+
38
+ @logger.info("EventSystem initialized with #{@config.storage_type} storage")
39
+ end
40
+
41
+ # Subscribe to events of a specific type
42
+ # @param event_type [String, Symbol] The event type to subscribe to
43
+ # @param subscriber [Object] The subscriber that will handle the events
44
+ # @return [void]
45
+ def subscribe(event_type, subscriber)
46
+ event_type = event_type.to_s
47
+ @subscribers[event_type] << subscriber
48
+ @logger.debug("Subscribed #{subscriber.class} to #{event_type}")
49
+ end
50
+
51
+ # Unsubscribe from events of a specific type
52
+ # @param event_type [String, Symbol] The event type to unsubscribe from
53
+ # @param subscriber [Object] The subscriber to remove
54
+ # @return [void]
55
+ def unsubscribe(event_type, subscriber)
56
+ event_type = event_type.to_s
57
+ @subscribers[event_type].delete(subscriber)
58
+ @logger.debug("Unsubscribed #{subscriber.class} from #{event_type}")
59
+ end
60
+
61
+ # Subscribe to all event types
62
+ # @param subscriber [Object] The subscriber that will handle all events
63
+ # @return [void]
64
+ def subscribe_all(subscriber)
65
+ @subscribers[:all] << subscriber
66
+ @logger.debug("Subscribed #{subscriber.class} to all events")
67
+ end
68
+
69
+ # Unsubscribe from all event types
70
+ # @param subscriber [Object] The subscriber to remove
71
+ # @return [void]
72
+ def unsubscribe_all(subscriber)
73
+ @subscribers[:all].delete(subscriber)
74
+ @logger.debug("Unsubscribed #{subscriber.class} from all events")
75
+ end
76
+
77
+ # Publish an event to all subscribers
78
+ # @param event [EventSystem::Event] The event to publish
79
+ # @return [void]
80
+ def publish(event)
81
+ @logger.debug("Publishing event: #{event}")
82
+
83
+ # Store the event if storage is configured
84
+ @storage&.store(event)
85
+
86
+ # Deliver to type-specific subscribers
87
+ deliver_to_subscribers(event.type, event)
88
+
89
+ # Deliver to universal subscribers
90
+ deliver_to_subscribers(:all, event)
91
+ end
92
+
93
+ # Create and publish an event in one step
94
+ # @param type [String, Symbol] The event type
95
+ # @param source [Object] The source of the event
96
+ # @param data [Hash] Additional event data
97
+ # @return [EventSystem::Event] The published event
98
+ def publish_event(type, source = nil, data = {})
99
+ @logger.debug("Publishing event: #{type}, source: #{source}, data: #{data}")
100
+ event = Event.new(type, source, data)
101
+ publish(event)
102
+ event
103
+ end
104
+
105
+ # Query for events based on options
106
+ # @param options [Hash] Query options
107
+ # @return [Array<EventSystem::Event>] Matching events
108
+ def query_events(options = {})
109
+ @storage&.query(options) || []
110
+ end
111
+
112
+ # Get the current session ID
113
+ # @return [String, nil] The current session ID, or nil if no event store
114
+ def current_session
115
+ @storage&.current_session
116
+ end
117
+
118
+ # Switch to a different session
119
+ # @param session_id [String] The session ID to switch to
120
+ # @return [void]
121
+ def switch_session(session_id)
122
+ @storage&.switch_session(session_id)
123
+ @logger.info("Switched to session: #{session_id}")
124
+ end
125
+
126
+ # Create a new session
127
+ # @param session_id [String, nil] Optional session ID, defaults to timestamp
128
+ # @return [String] The new session ID
129
+ def create_session(session_id = nil)
130
+ session_id = @storage&.create_session(session_id) || session_id || Time.now.strftime("%Y%m%d_%H%M%S")
131
+ @logger.info("Created new session: #{session_id}")
132
+ session_id
133
+ end
134
+
135
+ # Get statistics about the event system
136
+ # @return [Hash] Statistics about the event system
137
+ def stats
138
+ {
139
+ subscribers: @subscribers.transform_values(&:length),
140
+ storage: @storage&.stats || {},
141
+ config: @config.to_h
142
+ }
143
+ end
144
+
145
+ # Close the event manager and release resources
146
+ # @return [void]
147
+ def close
148
+ @storage&.close
149
+ @logger.info("EventSystem closed")
150
+ end
151
+
152
+ # Check if the event manager is ready for use
153
+ # @return [Boolean] True if ready
154
+ def ready?
155
+ @storage&.available? || false
156
+ end
157
+
158
+ private
159
+
160
+ # Deliver an event to subscribers of a specific type
161
+ # @param event_type [String] The event type
162
+ # @param event [EventSystem::Event] The event to deliver
163
+ # @return [void]
164
+ def deliver_to_subscribers(event_type, event)
165
+ subscribers = @subscribers[event_type] || []
166
+
167
+ # Sort subscribers by priority if they support it
168
+ sorted_subscribers = subscribers.sort_by do |subscriber|
169
+ if subscriber.respond_to?(:event_priority)
170
+ subscriber.event_priority
171
+ else
172
+ 0
173
+ end
174
+ end
175
+
176
+ sorted_subscribers.each do |subscriber|
177
+ begin
178
+ # Check if subscriber handles this event type
179
+ if subscriber.respond_to?(:handles_event_type?) && !subscriber.handles_event_type?(event_type)
180
+ next
181
+ end
182
+
183
+ subscriber.handle_event(event)
184
+ rescue => e
185
+ @logger.error("Error in subscriber #{subscriber.class} handling #{event_type}: #{e.message}")
186
+ @logger.error(e.backtrace.join("\n"))
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventSystem
4
+ # Interface for components that respond to events
5
+ # Classes including this module must implement the handle_event method
6
+ module EventSubscriber
7
+ # Handle an event that this subscriber is interested in
8
+ # @param event [EventSystem::Event] The event to handle
9
+ # @return [void]
10
+ def handle_event(event)
11
+ raise NotImplementedError, "#{self.class} must implement handle_event(event)"
12
+ end
13
+
14
+ # Check if this subscriber is interested in a specific event type
15
+ # Override this method to provide filtering logic
16
+ # @param event_type [String] The event type to check
17
+ # @return [Boolean] True if this subscriber handles the event type
18
+ def handles_event_type?(event_type)
19
+ true # Default: handle all event types
20
+ end
21
+
22
+ # Get the priority for this subscriber (lower numbers = higher priority)
23
+ # Override this method to control event handling order
24
+ # @return [Integer] Priority level (default: 0)
25
+ def event_priority
26
+ 0
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventSystem
4
+ module Storage
5
+ # Abstract base class for event storage implementations
6
+ # All concrete event store classes should implement these methods
7
+ class Base
8
+ # Store an event
9
+ # @param event [EventSystem::Event] The event to store
10
+ # @return [void]
11
+ def store(event)
12
+ raise NotImplementedError, "#{self.class} must implement store(event)"
13
+ end
14
+
15
+ # Query for events based on options
16
+ # @param options [Hash] Query options (type, time range, limit, etc.)
17
+ # @return [Array<EventSystem::Event>] Matching events
18
+ def query(options = {})
19
+ raise NotImplementedError, "#{self.class} must implement query(options)"
20
+ end
21
+
22
+ # Load all events from a session
23
+ # @param session_id [String, nil] Session ID to load, or current session if nil
24
+ # @return [Array<EventSystem::Event>] Events from the session
25
+ def load_session(session_id = nil)
26
+ raise NotImplementedError, "#{self.class} must implement load_session(session_id)"
27
+ end
28
+
29
+ # List available sessions
30
+ # @return [Array<String>] List of session IDs
31
+ def list_sessions
32
+ raise NotImplementedError, "#{self.class} must implement list_sessions"
33
+ end
34
+
35
+ # Get the current session ID
36
+ # @return [String, nil] Current session ID
37
+ def current_session
38
+ raise NotImplementedError, "#{self.class} must implement current_session"
39
+ end
40
+
41
+ # Close the event store and release resources
42
+ # @return [void]
43
+ def close
44
+ # Optional method, default implementation does nothing
45
+ end
46
+
47
+ # Check if the storage is available
48
+ # @return [Boolean] True if storage is ready for use
49
+ def available?
50
+ true
51
+ end
52
+
53
+ # Get storage statistics
54
+ # @return [Hash] Storage statistics
55
+ def stats
56
+ {
57
+ type: self.class.name,
58
+ available: available?,
59
+ sessions: list_sessions.length
60
+ }
61
+ end
62
+ end
63
+ end
64
+ end