adhearsion 0.8.6 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +24 -0
- data/Rakefile +3 -1
- data/adhearsion.gemspec +21 -4
- data/app_generators/ahn/templates/components/disabled/restful_rpc/restful_rpc.rb +6 -2
- data/app_generators/ahn/templates/config/startup.rb +11 -2
- data/lib/adhearsion.rb +7 -1
- data/lib/adhearsion/component_manager.rb +67 -2
- data/lib/adhearsion/foundation/all.rb +7 -1
- data/lib/adhearsion/foundation/blank_slate.rb +1 -3
- data/lib/adhearsion/initializer.rb +1 -2
- data/lib/adhearsion/initializer/asterisk.rb +2 -2
- data/lib/adhearsion/initializer/configuration.rb +16 -5
- data/lib/adhearsion/tasks.rb +1 -0
- data/lib/adhearsion/tasks/components.rb +32 -0
- data/lib/adhearsion/version.rb +3 -3
- data/lib/adhearsion/voip/asterisk/commands.rb +44 -16
- data/lib/adhearsion/voip/asterisk/config_generators/voicemail.conf.rb +2 -2
- data/lib/adhearsion/voip/asterisk/manager_interface.rb +14 -13
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rb +134 -134
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rl.rb +63 -10
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_messages.rb +1 -1
- data/lib/adhearsion/voip/asterisk/special_dial_plan_managers.rb +2 -2
- data/lib/adhearsion/voip/call.rb +11 -11
- data/lib/adhearsion/voip/call_routing.rb +1 -1
- data/lib/adhearsion/voip/dial_plan.rb +15 -6
- data/lib/adhearsion/voip/dsl/dialing_dsl.rb +1 -1
- data/lib/adhearsion/voip/dsl/dialing_dsl/dialing_dsl_monkey_patches.rb +1 -1
- data/lib/adhearsion/voip/dsl/dialplan/dispatcher.rb +1 -1
- data/lib/adhearsion/voip/dsl/dialplan/parser.rb +4 -6
- data/lib/adhearsion/voip/dsl/numerical_string.rb +1 -3
- data/lib/adhearsion/voip/freeswitch/basic_connection_manager.rb +2 -2
- data/lib/adhearsion/voip/freeswitch/freeswitch_dialplan_command_factory.rb +2 -2
- data/lib/adhearsion/voip/freeswitch/oes_server.rb +1 -1
- data/lib/adhearsion/voip/menu_state_machine/matchers.rb +1 -1
- data/lib/adhearsion/voip/menu_state_machine/menu_builder.rb +1 -1
- data/lib/theatre/callback_definition_loader.rb +1 -1
- metadata +78 -19
data/CHANGELOG
CHANGED
@@ -1,9 +1,33 @@
|
|
1
|
+
1.0.0
|
2
|
+
- Fall back to using Asterisk's context if the AGI URI context is not found
|
3
|
+
- Enable configuration of :auto_reconnect parameter for AMI
|
4
|
+
- Replace all uses of Object#returning with Object#tap
|
5
|
+
- Add support for loading Adhearsion components from RubyGems
|
6
|
+
- Fix long-running AMI session parser failure bug (#72)
|
7
|
+
- Support for Rails 3 (and ActiveSupport 3.0)
|
8
|
+
|
1
9
|
0.8.6
|
2
10
|
- Fix packaging problem so all files are publicly readable
|
3
11
|
- Improve AMI reconnecting logic; add "connection refused" retry timer
|
4
12
|
- AGI protocol improvements: parse the status code and response text
|
5
13
|
|
6
14
|
0.8.5
|
15
|
+
NOTE: If you are upgrading an Adhearsion application to 0.8.5, note the change
|
16
|
+
to how request URIs are handled. With 0.8.4, the context name in Asterisk was
|
17
|
+
required to match the Adhearsion context in dialplan.rb. Starting in 0.8.5 if
|
18
|
+
an application path is passed in on the AGI URI, it will be preferred over the
|
19
|
+
context name. For example:
|
20
|
+
|
21
|
+
[stuff]
|
22
|
+
exten => _X.,1,AGI(agi://localhost/myapp)
|
23
|
+
|
24
|
+
AHN 0.8.4- will execute the "stuff" context in dialplan.rb
|
25
|
+
AHN 0.8.5+ will execute the "myapp" context in dialplan.rb
|
26
|
+
|
27
|
+
If you followed the documentation and did not specify an application path in
|
28
|
+
the URI (eg. agi://localhost) you will not be impacted by this change.
|
29
|
+
|
30
|
+
Other changes:
|
7
31
|
- Added XMPP module and sample component. This allows you to easily write components which utilise a persistent XMPP connection maintained by Adhearsion
|
8
32
|
- Prefer finding the dialplan.rb entry point by the AGI request URI instead of the calling context
|
9
33
|
- Added :use_static_conf option for "meetme" to allow the use of disk-file-managed conferences
|
data/Rakefile
CHANGED
@@ -4,11 +4,13 @@ ENV['RUBY_FLAGS'] = "-I#{%w(lib ext bin test).join(File::PATH_SEPARATOR)}"
|
|
4
4
|
require 'rubygems'
|
5
5
|
require 'rake/gempackagetask'
|
6
6
|
require 'rake/testtask'
|
7
|
+
require 'date'
|
7
8
|
|
8
9
|
begin
|
10
|
+
gem 'rspec', '~> 1.3.0'
|
9
11
|
require 'spec/rake/spectask'
|
10
12
|
rescue LoadError
|
11
|
-
abort "You must install RSpec: sudo gem install rspec"
|
13
|
+
abort "You must install RSpec 1.3: sudo gem install rspec -v'<2.0.0'"
|
12
14
|
end
|
13
15
|
|
14
16
|
begin
|
data/adhearsion.gemspec
CHANGED
@@ -60,6 +60,7 @@ ADHEARSION_FILES = %w{
|
|
60
60
|
lib/adhearsion/initializer/xmpp.rb
|
61
61
|
lib/adhearsion/logging.rb
|
62
62
|
lib/adhearsion/tasks.rb
|
63
|
+
lib/adhearsion/tasks/components.rb
|
63
64
|
lib/adhearsion/tasks/database.rb
|
64
65
|
lib/adhearsion/tasks/deprecations.rb
|
65
66
|
lib/adhearsion/tasks/generating.rb
|
@@ -122,7 +123,7 @@ Gem::Specification.new do |s|
|
|
122
123
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
123
124
|
s.authors = ["Jay Phillips", "Jason Goecke", "Ben Klang"]
|
124
125
|
|
125
|
-
s.date =
|
126
|
+
s.date = Date.today.to_s
|
126
127
|
s.description = "Adhearsion is an open-source telephony development framework"
|
127
128
|
s.email = "dev&Adhearsion.com"
|
128
129
|
s.executables = ["ahn", "ahnctl", "jahn"]
|
@@ -135,22 +136,38 @@ Gem::Specification.new do |s|
|
|
135
136
|
s.rubyforge_project = "adhearsion"
|
136
137
|
s.rubygems_version = "1.2.0"
|
137
138
|
s.summary = "Adhearsion, open-source telephony development framework"
|
139
|
+
s.post_install_message =<<-EOM
|
140
|
+
*******************************************************************
|
141
|
+
* NOTE: You must manually install the "rubigen" gem to create *
|
142
|
+
* new Adhearsion applications. *
|
143
|
+
* *
|
144
|
+
* The Rubigen package is no longer automatically installed due to *
|
145
|
+
* dependency conflicts with ActiveSupport 3.0. *
|
146
|
+
* Users of existing Adhearsion applications can safely ignore *
|
147
|
+
* this message. *
|
148
|
+
*******************************************************************
|
149
|
+
EOM
|
138
150
|
|
139
151
|
if s.respond_to? :specification_version then
|
140
152
|
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
141
153
|
s.specification_version = 2
|
142
154
|
|
143
155
|
if current_version >= 3 then
|
144
|
-
|
156
|
+
# Runtime dependencies
|
145
157
|
s.add_runtime_dependency("log4r", [">= 1.0.5"])
|
146
158
|
s.add_runtime_dependency("activesupport", [">= 2.1.0"])
|
159
|
+
|
160
|
+
# Development dependencies
|
161
|
+
s.add_development_dependency('rubigen', [">= 1.0.6"])
|
162
|
+
s.add_development_dependency('rspec', ["< 2.0.0"])
|
163
|
+
s.add_development_dependency('test-unit')
|
164
|
+
s.add_development_dependency('flexmock')
|
165
|
+
s.add_development_dependency('active_record')
|
147
166
|
else
|
148
|
-
s.add_dependency("rubigen", [">= 1.0.6"])
|
149
167
|
s.add_dependency("log4r", [">= 1.0.5"])
|
150
168
|
s.add_dependency("activesupport", [">= 2.1.0"])
|
151
169
|
end
|
152
170
|
else
|
153
|
-
s.add_dependency("rubigen", [">= 1.0.6"])
|
154
171
|
s.add_dependency("log4r", [">= 1.0.5"])
|
155
172
|
s.add_dependency("activesupport", [">= 2.1.0"])
|
156
173
|
end
|
@@ -1,5 +1,9 @@
|
|
1
|
-
|
2
|
-
require '
|
1
|
+
begin
|
2
|
+
require 'rack'
|
3
|
+
require 'json'
|
4
|
+
rescue LoadError
|
5
|
+
abort "ERROR: restful_rpc requires the 'rack' and 'json' gems"
|
6
|
+
end
|
3
7
|
|
4
8
|
# Don't you love regular expressions? Matches only 0-255 octets. Recognizes "*" as an octet wildcard.
|
5
9
|
VALID_IP_ADDRESS = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|\*)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|\*)$/
|
@@ -1,7 +1,9 @@
|
|
1
1
|
unless defined? Adhearsion
|
2
2
|
if File.exists? File.dirname(__FILE__) + "/../adhearsion/lib/adhearsion.rb"
|
3
|
-
#
|
4
|
-
#
|
3
|
+
# For development purposes try to load a local copy of Adhearsion here.
|
4
|
+
# This will not work if started using "ahn" or "jahn". You must execute
|
5
|
+
# config/startup.rb directly and have a local checkout of Adhearsion in your
|
6
|
+
# application directory.
|
5
7
|
require File.dirname(__FILE__) + "/../adhearsion/lib/adhearsion.rb"
|
6
8
|
else
|
7
9
|
require 'rubygems'
|
@@ -12,6 +14,13 @@ end
|
|
12
14
|
|
13
15
|
Adhearsion::Configuration.configure do |config|
|
14
16
|
|
17
|
+
# Components to load from the system.
|
18
|
+
# All components that are activated in components/ will be automatically
|
19
|
+
# loaded and made available.
|
20
|
+
# This configuration option allows you to load components provided by gems.
|
21
|
+
# List the gem names here:
|
22
|
+
# config.add_component "ahn_test_component"
|
23
|
+
|
15
24
|
# Supported levels (in increasing severity) -- :debug < :info < :warn < :error < :fatal
|
16
25
|
config.logging :level => :info
|
17
26
|
|
data/lib/adhearsion.rb
CHANGED
@@ -24,7 +24,13 @@ require 'adhearsion/voip/asterisk/commands'
|
|
24
24
|
require 'adhearsion/voip/dsl/dialing_dsl'
|
25
25
|
require 'adhearsion/voip/call_routing'
|
26
26
|
|
27
|
-
|
27
|
+
begin
|
28
|
+
# Try ActiveSupport >= 2.3.0
|
29
|
+
require 'active_support/all'
|
30
|
+
rescue LoadError
|
31
|
+
# Assume ActiveSupport < 2.3.0
|
32
|
+
require 'active_support'
|
33
|
+
end
|
28
34
|
|
29
35
|
module Adhearsion
|
30
36
|
# Sets up the Gem require path.
|
@@ -43,6 +43,13 @@ module Adhearsion
|
|
43
43
|
components.map! { |path| File.basename path }
|
44
44
|
components.each do |component|
|
45
45
|
next if component == "disabled"
|
46
|
+
component_file = File.join(@path_to_container_directory, component, 'lib', component + ".rb")
|
47
|
+
if File.exists? component_file
|
48
|
+
load_file component_file
|
49
|
+
next
|
50
|
+
end
|
51
|
+
|
52
|
+
# Try the old-style components/<component>/<component>.rb
|
46
53
|
component_file = File.join(@path_to_container_directory, component, component + ".rb")
|
47
54
|
if File.exists? component_file
|
48
55
|
load_file component_file
|
@@ -51,6 +58,11 @@ module Adhearsion
|
|
51
58
|
end
|
52
59
|
end
|
53
60
|
|
61
|
+
# Load configured system- or gem-provided components
|
62
|
+
AHN_CONFIG.components_to_load.each do |component|
|
63
|
+
require component
|
64
|
+
end
|
65
|
+
|
54
66
|
end
|
55
67
|
|
56
68
|
##
|
@@ -59,11 +71,19 @@ module Adhearsion
|
|
59
71
|
# @return [Hash] The loaded YAML for the given component name. An empty Hash if no YAML file exists.
|
60
72
|
#
|
61
73
|
def configuration_for_component_named(component_name)
|
74
|
+
# Look for configuration in #{AHN_ROOT}/config/components first
|
75
|
+
if File.exists?("#{AHN_ROOT}/config/components/#{component_name}.yml")
|
76
|
+
return YAML.load_file "#{AHN_ROOT}/config/components/#{component_name}.yml"
|
77
|
+
end
|
78
|
+
|
79
|
+
# Next try the local app component directory
|
62
80
|
component_dir = File.join(@path_to_container_directory, component_name)
|
63
81
|
config_file = File.join component_dir, "#{component_name}.yml"
|
64
82
|
if File.exists?(config_file)
|
65
83
|
YAML.load_file config_file
|
66
84
|
else
|
85
|
+
# Nothing found? Return an empty hash
|
86
|
+
ahn_log.warn "No configuration found for requested component #{component_name}"
|
67
87
|
return {}
|
68
88
|
end
|
69
89
|
end
|
@@ -92,6 +112,10 @@ module Adhearsion
|
|
92
112
|
load_container ComponentDefinitionContainer.load_file(filename)
|
93
113
|
end
|
94
114
|
|
115
|
+
def require(filename)
|
116
|
+
load_container ComponentDefinitionContainer.require(filename)
|
117
|
+
end
|
118
|
+
|
95
119
|
protected
|
96
120
|
|
97
121
|
def load_container(container)
|
@@ -116,16 +140,37 @@ module Adhearsion
|
|
116
140
|
|
117
141
|
class << self
|
118
142
|
def load_code(code)
|
119
|
-
|
143
|
+
new.tap do |instance|
|
120
144
|
instance.module_eval code
|
121
145
|
end
|
122
146
|
end
|
123
147
|
|
124
148
|
def load_file(filename)
|
125
|
-
|
149
|
+
new.tap do |instance|
|
126
150
|
instance.module_eval File.read(filename), filename
|
127
151
|
end
|
128
152
|
end
|
153
|
+
|
154
|
+
def require(filename)
|
155
|
+
filename = filename + ".rb" if !(filename =~ /\.rb$/)
|
156
|
+
begin
|
157
|
+
# Try loading the exact filename first
|
158
|
+
load_file(filename)
|
159
|
+
rescue LoadError, Errno::ENOENT
|
160
|
+
end
|
161
|
+
|
162
|
+
# Next try Rubygems
|
163
|
+
filepath = get_gem_path_for(filename)
|
164
|
+
return load_file(filepath) if !filepath.nil?
|
165
|
+
|
166
|
+
# Finally try the system search path
|
167
|
+
filepath = get_system_path_for(filename)
|
168
|
+
return load_file(filepath) if !filepath.nil?
|
169
|
+
|
170
|
+
# Raise a LoadError exception if the file is still not found
|
171
|
+
raise LoadError, "File not found: #{filename}"
|
172
|
+
end
|
173
|
+
|
129
174
|
end
|
130
175
|
|
131
176
|
def initialize(&block)
|
@@ -170,6 +215,26 @@ module Adhearsion
|
|
170
215
|
@methods ||= []
|
171
216
|
@methods << method_name
|
172
217
|
end
|
218
|
+
|
219
|
+
def get_gem_path_for(filename)
|
220
|
+
# Look for component files provided by rubygems
|
221
|
+
spec = Gem.searcher.find(filename)
|
222
|
+
return nil if spec.nil?
|
223
|
+
File.join(spec.full_gem_path, spec.require_path, filename)
|
224
|
+
rescue NameError
|
225
|
+
# In case Rubygems are not available
|
226
|
+
nil
|
227
|
+
end
|
228
|
+
|
229
|
+
def get_system_path_for(filename)
|
230
|
+
$:.each do |path|
|
231
|
+
filepath = File.join(path, filename)
|
232
|
+
return filepath if File.exists?(filepath)
|
233
|
+
end
|
234
|
+
|
235
|
+
# Not found? Return nil
|
236
|
+
return nil
|
237
|
+
end
|
173
238
|
end
|
174
239
|
|
175
240
|
end
|
@@ -1,7 +1,13 @@
|
|
1
1
|
require 'English'
|
2
2
|
require 'tmpdir'
|
3
3
|
require 'tempfile'
|
4
|
-
|
4
|
+
begin
|
5
|
+
# Try ActiveSupport >= 2.3.0
|
6
|
+
require 'active_support/all'
|
7
|
+
rescue LoadError
|
8
|
+
# Assume ActiveSupport < 2.3.0
|
9
|
+
require 'active_support'
|
10
|
+
end
|
5
11
|
|
6
12
|
# Require all other files here.
|
7
13
|
Dir.glob File.join(File.dirname(__FILE__), "*rb") do |file|
|
@@ -128,11 +128,10 @@ module Adhearsion
|
|
128
128
|
@daemon = options[:daemon] || ENV['DAEMON']
|
129
129
|
@pid_file = options[:pid_file].nil? ? ENV['PID_FILE'] : options[:pid_file]
|
130
130
|
@loaded_init_files = options[:loaded_init_files]
|
131
|
+
self.class.ahn_root = path
|
131
132
|
end
|
132
133
|
|
133
134
|
def start
|
134
|
-
self.class.ahn_root = path
|
135
|
-
|
136
135
|
Adhearsion.status = :starting
|
137
136
|
|
138
137
|
resolve_pid_file_path
|
@@ -35,7 +35,7 @@ module Adhearsion
|
|
35
35
|
def initialize_ami
|
36
36
|
options = ami_options
|
37
37
|
start_ami_after_initialized
|
38
|
-
|
38
|
+
VoIP::Asterisk::Manager::ManagerInterface.new(options).tap do
|
39
39
|
class << VoIP::Asterisk
|
40
40
|
if respond_to?(:manager_interface)
|
41
41
|
ahn_log.warn "Asterisk.manager_interface already initialized?"
|
@@ -50,7 +50,7 @@ module Adhearsion
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def ami_options
|
53
|
-
%w(host port username password events).inject({}) do |options, property|
|
53
|
+
%w(host port username password events auto_reconnect).inject({}) do |options, property|
|
54
54
|
options[property.to_sym] = config.ami.send property
|
55
55
|
options
|
56
56
|
end
|
@@ -35,11 +35,13 @@ module Adhearsion
|
|
35
35
|
attr_accessor :automatically_answer_incoming_calls
|
36
36
|
attr_accessor :end_call_on_hangup
|
37
37
|
attr_accessor :end_call_on_error
|
38
|
+
attr_accessor :components_to_load
|
38
39
|
|
39
40
|
def initialize
|
40
41
|
@automatically_answer_incoming_calls = true
|
41
42
|
@end_call_on_hangup = true
|
42
43
|
@end_call_on_error = true
|
44
|
+
@components_to_load = []
|
43
45
|
yield self if block_given?
|
44
46
|
end
|
45
47
|
|
@@ -67,6 +69,10 @@ module Adhearsion
|
|
67
69
|
Adhearsion::Logging.logging_level = options[:level]
|
68
70
|
end
|
69
71
|
|
72
|
+
def add_component(*list)
|
73
|
+
AHN_CONFIG.components_to_load |= list
|
74
|
+
end
|
75
|
+
|
70
76
|
##
|
71
77
|
# Adhearsion's .ahnrc file is used to define paths to certain parts of the framework. For example, the name dialplan.rb
|
72
78
|
# is actually specified in .ahnrc. This file can actually be just a filename, a filename with a glob (.e.g "*.rb"), an
|
@@ -129,8 +135,8 @@ module Adhearsion
|
|
129
135
|
end
|
130
136
|
|
131
137
|
def initialize(overrides = {})
|
132
|
-
@listening_port = overrides.has_key?(:port) ? overrides.delete(:port) : self.class.default_listening_port
|
133
138
|
@listening_host = overrides.has_key?(:host) ? overrides.delete(:host) : self.class.default_listening_host
|
139
|
+
@listening_port = overrides.has_key?(:port) ? overrides.delete(:port) : self.class.default_listening_port
|
134
140
|
super
|
135
141
|
end
|
136
142
|
end
|
@@ -158,7 +164,7 @@ module Adhearsion
|
|
158
164
|
end
|
159
165
|
|
160
166
|
class AMIConfiguration < AbstractConfiguration
|
161
|
-
attr_accessor :port, :username, :password, :events, :host
|
167
|
+
attr_accessor :port, :username, :password, :events, :host, :auto_reconnect
|
162
168
|
|
163
169
|
class << self
|
164
170
|
def default_port
|
@@ -172,12 +178,17 @@ module Adhearsion
|
|
172
178
|
def default_host
|
173
179
|
'localhost'
|
174
180
|
end
|
181
|
+
|
182
|
+
def default_auto_reconnect
|
183
|
+
true
|
184
|
+
end
|
175
185
|
end
|
176
186
|
|
177
187
|
def initialize(overrides = {})
|
178
|
-
self.host
|
179
|
-
self.port
|
180
|
-
self.events
|
188
|
+
self.host = self.class.default_host
|
189
|
+
self.port = self.class.default_port
|
190
|
+
self.events = self.class.default_events
|
191
|
+
self.auto_reconnect = self.class.default_auto_reconnect
|
181
192
|
super
|
182
193
|
end
|
183
194
|
end
|
data/lib/adhearsion/tasks.rb
CHANGED
@@ -0,0 +1,32 @@
|
|
1
|
+
namespace:components do
|
2
|
+
desc "Install component configuration from templates"
|
3
|
+
task :genconfig do
|
4
|
+
init = Adhearsion::Initializer.new(Rake.original_dir)
|
5
|
+
init.bootstrap_rc
|
6
|
+
init.load_all_init_files
|
7
|
+
Adhearsion::AHN_CONFIG.components_to_load.each do |component|
|
8
|
+
spec = Gem.searcher.find(component)
|
9
|
+
if spec.nil?
|
10
|
+
abort "ERROR: Required gem component #{component} not found."
|
11
|
+
end
|
12
|
+
|
13
|
+
yml = File.join(spec.full_gem_path, 'config', "#{component}.yml")
|
14
|
+
target = File.join(AHN_ROOT, 'config', 'components', "#{component}.yml")
|
15
|
+
Dir.mkdir(File.dirname(target)) if !File.exists?(File.dirname(target))
|
16
|
+
if File.exists?(target)
|
17
|
+
puts "Skipping existing configuration for component #{component}"
|
18
|
+
next
|
19
|
+
end
|
20
|
+
if File.exists?(yml)
|
21
|
+
begin
|
22
|
+
FileUtils.cp(yml, target)
|
23
|
+
puts "Installed default configuration for component #{component}"
|
24
|
+
rescue => e
|
25
|
+
abort "Error copying configuration for component #{component}: #{e.message}"
|
26
|
+
end
|
27
|
+
else
|
28
|
+
puts "No template configuration found for component #{component}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/adhearsion/version.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
module Adhearsion #:nodoc:
|
2
2
|
module VERSION #:nodoc:
|
3
|
-
MAJOR =
|
4
|
-
MINOR =
|
5
|
-
TINY =
|
3
|
+
MAJOR = 1 unless defined? MAJOR
|
4
|
+
MINOR = 0 unless defined? MINOR
|
5
|
+
TINY = 0 unless defined? TINY
|
6
6
|
|
7
7
|
STRING = [MAJOR, MINOR, TINY].join('.') unless defined? STRING
|
8
8
|
end
|