conduit-ussd 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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +426 -0
  4. data/Rakefile +2 -0
  5. data/app/assets/stylesheets/conduit/application.css +15 -0
  6. data/app/controllers/conduit/application_controller.rb +4 -0
  7. data/app/helpers/conduit/application_helper.rb +4 -0
  8. data/app/jobs/conduit/application_job.rb +4 -0
  9. data/app/jobs/conduit/save_session_job.rb +11 -0
  10. data/app/models/conduit/application_record.rb +5 -0
  11. data/app/models/conduit/session_record.rb +28 -0
  12. data/config/routes.rb +2 -0
  13. data/lib/conduit/configuration.rb +25 -0
  14. data/lib/conduit/display_builder.rb +58 -0
  15. data/lib/conduit/engine.rb +21 -0
  16. data/lib/conduit/flow.rb +54 -0
  17. data/lib/conduit/middleware/logging.rb +23 -0
  18. data/lib/conduit/middleware/session_tracking.rb +30 -0
  19. data/lib/conduit/middleware/throttling.rb +41 -0
  20. data/lib/conduit/middleware.rb +36 -0
  21. data/lib/conduit/providers/africas_talking.rb +39 -0
  22. data/lib/conduit/request_handler.rb +126 -0
  23. data/lib/conduit/response.rb +23 -0
  24. data/lib/conduit/router.rb +30 -0
  25. data/lib/conduit/session.rb +73 -0
  26. data/lib/conduit/session_store.rb +41 -0
  27. data/lib/conduit/state.rb +189 -0
  28. data/lib/conduit/validator.rb +55 -0
  29. data/lib/conduit/version.rb +3 -0
  30. data/lib/conduit.rb +31 -0
  31. data/lib/generators/conduit/install/install_generator.rb +41 -0
  32. data/lib/generators/conduit/install/templates/conduit.rb +26 -0
  33. data/lib/generators/conduit/install/templates/example_flow.rb +72 -0
  34. data/lib/generators/conduit/install/templates/ussd_controller.rb +11 -0
  35. data/lib/generators/conduit/migration/migration_generator.rb +21 -0
  36. data/lib/generators/conduit/migration/templates/create_conduit_sessions.rb +19 -0
  37. data/lib/tasks/conduit_tasks.rake +4 -0
  38. metadata +191 -0
@@ -0,0 +1,55 @@
1
+ module Conduit
2
+ class Validator
3
+ def self.numeric
4
+ lambda do |input, _session|
5
+ return true if input.match?(/^\d+(\.\d+)?$/)
6
+ "Please enter a valid number"
7
+ end
8
+ end
9
+
10
+ def self.greater_than(value)
11
+ lambda do |input, _session|
12
+ num = input.to_f
13
+ return true if num > value
14
+ "Must be greater than #{value}"
15
+ end
16
+ end
17
+
18
+ def self.less_than(value)
19
+ lambda do |input, _session|
20
+ num = input.to_f
21
+ return true if num < value
22
+ "Must be less than #{value}"
23
+ end
24
+ end
25
+
26
+ def self.min_length(length)
27
+ lambda do |input, _session|
28
+ return true if input.length >= length
29
+ "Must be at least #{length} characters"
30
+ end
31
+ end
32
+
33
+ def self.max_length(length)
34
+ lambda do |input, _session|
35
+ return true if input.length <= length
36
+ "Must be at most #{length} characters"
37
+ end
38
+ end
39
+
40
+ def self.matches(pattern, message = "Invalid format")
41
+ lambda do |input, _session|
42
+ return true if input.match?(pattern)
43
+ message
44
+ end
45
+ end
46
+
47
+ def self.in_range(min, max)
48
+ lambda do |input, _session|
49
+ num = input.to_f
50
+ return true if num.between?(min, max)
51
+ "Must be between #{min} and #{max}"
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,3 @@
1
+ module Conduit
2
+ VERSION = "0.1.0"
3
+ end
data/lib/conduit.rb ADDED
@@ -0,0 +1,31 @@
1
+ require "conduit/version"
2
+ require "conduit/engine"
3
+ require "conduit/configuration"
4
+ require "conduit/router"
5
+ require "conduit/middleware"
6
+ require "conduit/middleware/logging"
7
+ require "conduit/middleware/throttling"
8
+ require "conduit/middleware/session_tracking"
9
+ require "conduit/display_builder"
10
+ require "conduit/validator"
11
+
12
+ module Conduit
13
+ class Error < StandardError; end
14
+
15
+ class SessionTimeout < Error; end
16
+
17
+ class InvalidTransition < Error; end
18
+
19
+ mattr_accessor :configuration
20
+
21
+ class << self
22
+ def configure
23
+ self.configuration ||= Configuration.new
24
+ yield(configuration) if block_given?
25
+ end
26
+
27
+ def process(params)
28
+ RequestHandler.new.process(params)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,41 @@
1
+ require "rails/generators"
2
+
3
+ module Conduit
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ def create_initializer
9
+ template "conduit.rb", "config/initializers/conduit.rb"
10
+ end
11
+
12
+ def create_controller
13
+ template "ussd_controller.rb", "app/controllers/ussd_controller.rb"
14
+ end
15
+
16
+ def add_routes
17
+ route 'post "/ussd", to: "ussd#handle"'
18
+ end
19
+
20
+ def create_flow_directory
21
+ empty_directory "app/flows"
22
+ template "example_flow.rb", "app/flows/example_flow.rb"
23
+ end
24
+
25
+ def display_post_install
26
+ say "\n✅ Conduit installed successfully!", :green
27
+ say "\nNext steps:"
28
+ say " 1. Configure Redis in config/initializers/conduit.rb"
29
+ say " 2. Run 'rails generate conduit:migration' to create database table"
30
+ say " 3. Run 'rails db:migrate'"
31
+ say " 4. Create your flows in app/flows/"
32
+ say " 5. Map service codes to flows in the initializer"
33
+ say " 6. Test your USSD endpoint: POST /ussd"
34
+ say "\nExample AfricasTalking request:"
35
+ say " curl -X POST http://localhost:3000/ussd \\"
36
+ say ' -H "Content-Type: application/x-www-form-urlencoded" \\'
37
+ say ' -d "sessionId=test123&phoneNumber=254712345678&serviceCode=*123%23&text="'
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,26 @@
1
+ Conduit.configure do |config|
2
+ # Redis configuration
3
+ config.redis_url = ENV.fetch("REDIS_URL", "redis://localhost:6379/1")
4
+ config.session_ttl = 90.seconds
5
+
6
+ # Logging
7
+ config.logger = Rails.logger
8
+
9
+ # Session settings
10
+ config.max_navigation_depth = 10
11
+ config.save_sessions = true
12
+
13
+ # Middleware (order matters - first added is outermost)
14
+ config.middleware.use Conduit::Middleware::Logging
15
+ config.middleware.use Conduit::Middleware::Throttling, max_requests: 20, window: 60
16
+ config.middleware.use Conduit::Middleware::SessionTracking
17
+
18
+ # Add custom middleware
19
+ # config.middleware.use MyCustomMiddleware
20
+ end
21
+
22
+ # Route service codes to flows
23
+ Conduit::Router.draw do
24
+ route "*123#", to: ExampleFlow
25
+ # route "*456#", to: AnotherFlow
26
+ end
@@ -0,0 +1,72 @@
1
+ class ExampleFlow < Conduit::Flow
2
+ initial_state :welcome
3
+
4
+ state :welcome do
5
+ display <<~TEXT
6
+ Welcome to Conduit USSD Demo!
7
+
8
+ 1. Get Started
9
+ 2. About
10
+ 3. Exit
11
+ TEXT
12
+
13
+ on "1", to: :get_name
14
+ on "2", to: :about
15
+ on "3" do
16
+ Conduit::Response.new(text: "Thank you for using Conduit! Goodbye.", action: :end)
17
+ end
18
+ end
19
+
20
+ state :get_name do
21
+ display "What's your name?"
22
+
23
+ on_any do |input, session|
24
+ session.data[:name] = input
25
+ session.navigate_to(:greet)
26
+ end
27
+ end
28
+
29
+ state :greet do
30
+ display do |session|
31
+ <<~TEXT
32
+ Hello #{session.data[:name]}!
33
+
34
+ You have successfully set up Conduit.
35
+
36
+ 1. Try something else
37
+ 0. Back
38
+ 00. Home
39
+ TEXT
40
+ end
41
+
42
+ on "1", to: :demo_feature
43
+ end
44
+
45
+ state :about do
46
+ display <<~TEXT
47
+ Conduit v#{Conduit::VERSION}
48
+
49
+ Lightning-fast USSD framework for Rails
50
+ Built with ❤️ for Africa
51
+
52
+ 0. Back
53
+ 00. Home
54
+ TEXT
55
+ end
56
+
57
+ state :demo_feature do
58
+ display do |session|
59
+ <<~TEXT
60
+ #{session.data[:name]}, this is where you'd add your features!
61
+
62
+ Current session info:
63
+ - ID: #{session.session_id}
64
+ - Phone: #{session.msisdn}
65
+ - Duration: #{session.duration.to_i}s
66
+
67
+ 0. Back
68
+ 00. Home
69
+ TEXT
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,11 @@
1
+ class UssdController < ApplicationController
2
+ skip_before_action :verify_authenticity_token
3
+
4
+ def handle
5
+ # Process AfricasTalking USSD request
6
+ response = Conduit.process(params)
7
+
8
+ # Return plain text response for AfricasTalking
9
+ render plain: response, content_type: "text/plain"
10
+ end
11
+ end
@@ -0,0 +1,21 @@
1
+ require "rails/generators"
2
+ require "rails/generators/active_record"
3
+
4
+ module Conduit
5
+ module Generators
6
+ class MigrationGenerator < Rails::Generators::Base
7
+ include ActiveRecord::Generators::Migration
8
+
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ def create_migration_file
12
+ migration_template "create_conduit_sessions.rb", "db/migrate/create_conduit_sessions.rb"
13
+ end
14
+
15
+ def display_post_install
16
+ say "\n✅ Migration created!", :green
17
+ say "\nRun 'rails db:migrate' to create the conduit_sessions table"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ class CreateConduitSessions < ActiveRecord::Migration[8.0] # this needs fixing to make it dynamic?
2
+ def change
3
+ create_table :conduit_sessions do |t|
4
+ t.string :session_id, null: false, index: {unique: true}
5
+ t.string :msisdn, null: false, index: true
6
+ t.string :service_code
7
+ t.string :final_state
8
+ t.jsonb :data, default: {}
9
+ t.integer :duration_seconds
10
+ t.boolean :completed, default: false
11
+ t.datetime :started_at, null: false
12
+ t.datetime :completed_at
13
+ t.timestamps null: false
14
+
15
+ t.index [:msisdn, :created_at]
16
+ t.index :created_at
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :conduit do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,191 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: conduit-ussd
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Charles Chuck
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: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 8.0.2
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 8.0.2
26
+ - !ruby/object:Gem::Dependency
27
+ name: redis
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '5.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '5.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: connection_pool
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.4'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.4'
54
+ - !ruby/object:Gem::Dependency
55
+ name: standard
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rubocop-rails-omakase
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rspec-rails
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '8.0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '8.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: fakeredis
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: pry-rails
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ description: Build USSD applications with an expressive DSL. Redis-backed sessions,
125
+ AfricasTalking integration, built for the 60-120 second constraint.
126
+ email:
127
+ - chalcchuck@email.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - MIT-LICENSE
133
+ - README.md
134
+ - Rakefile
135
+ - app/assets/stylesheets/conduit/application.css
136
+ - app/controllers/conduit/application_controller.rb
137
+ - app/helpers/conduit/application_helper.rb
138
+ - app/jobs/conduit/application_job.rb
139
+ - app/jobs/conduit/save_session_job.rb
140
+ - app/models/conduit/application_record.rb
141
+ - app/models/conduit/session_record.rb
142
+ - config/routes.rb
143
+ - lib/conduit.rb
144
+ - lib/conduit/configuration.rb
145
+ - lib/conduit/display_builder.rb
146
+ - lib/conduit/engine.rb
147
+ - lib/conduit/flow.rb
148
+ - lib/conduit/middleware.rb
149
+ - lib/conduit/middleware/logging.rb
150
+ - lib/conduit/middleware/session_tracking.rb
151
+ - lib/conduit/middleware/throttling.rb
152
+ - lib/conduit/providers/africas_talking.rb
153
+ - lib/conduit/request_handler.rb
154
+ - lib/conduit/response.rb
155
+ - lib/conduit/router.rb
156
+ - lib/conduit/session.rb
157
+ - lib/conduit/session_store.rb
158
+ - lib/conduit/state.rb
159
+ - lib/conduit/validator.rb
160
+ - lib/conduit/version.rb
161
+ - lib/generators/conduit/install/install_generator.rb
162
+ - lib/generators/conduit/install/templates/conduit.rb
163
+ - lib/generators/conduit/install/templates/example_flow.rb
164
+ - lib/generators/conduit/install/templates/ussd_controller.rb
165
+ - lib/generators/conduit/migration/migration_generator.rb
166
+ - lib/generators/conduit/migration/templates/create_conduit_sessions.rb
167
+ - lib/tasks/conduit_tasks.rake
168
+ homepage: https://github.com/chalchuck/conduit
169
+ licenses:
170
+ - MIT
171
+ metadata:
172
+ homepage_uri: https://github.com/chalchuck/conduit
173
+ source_code_uri: https://github.com/chalchuck/conduit
174
+ rdoc_options: []
175
+ require_paths:
176
+ - lib
177
+ required_ruby_version: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - ">="
180
+ - !ruby/object:Gem::Version
181
+ version: 3.4.1
182
+ required_rubygems_version: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ requirements: []
188
+ rubygems_version: 3.6.9
189
+ specification_version: 4
190
+ summary: Lightning-fast USSD flow engine for Rails
191
+ test_files: []