adhearsion 0.8.6 → 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.
- 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
|