mobility 0.8.8 → 1.0.0.alpha
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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/CHANGELOG.md +56 -0
- data/Gemfile +52 -16
- data/Gemfile.lock +113 -52
- data/Guardfile +23 -1
- data/README.md +184 -92
- data/Rakefile +6 -4
- data/lib/mobility.rb +40 -166
- data/lib/mobility/active_record/translation.rb +1 -1
- data/lib/mobility/arel/nodes/pg_ops.rb +1 -1
- data/lib/mobility/backend.rb +19 -41
- data/lib/mobility/backends.rb +20 -0
- data/lib/mobility/backends/active_record.rb +4 -0
- data/lib/mobility/backends/active_record/column.rb +2 -0
- data/lib/mobility/backends/active_record/container.rb +4 -2
- data/lib/mobility/backends/active_record/hstore.rb +2 -0
- data/lib/mobility/backends/active_record/json.rb +2 -0
- data/lib/mobility/backends/active_record/jsonb.rb +2 -0
- data/lib/mobility/backends/active_record/key_value.rb +5 -3
- data/lib/mobility/backends/active_record/pg_hash.rb +1 -1
- data/lib/mobility/backends/active_record/serialized.rb +2 -0
- data/lib/mobility/backends/active_record/table.rb +5 -3
- data/lib/mobility/backends/column.rb +0 -6
- data/lib/mobility/backends/container.rb +2 -1
- data/lib/mobility/backends/hash.rb +39 -0
- data/lib/mobility/backends/hstore.rb +0 -1
- data/lib/mobility/backends/json.rb +0 -1
- data/lib/mobility/backends/jsonb.rb +0 -1
- data/lib/mobility/backends/key_value.rb +22 -14
- data/lib/mobility/backends/null.rb +2 -0
- data/lib/mobility/backends/sequel.rb +3 -0
- data/lib/mobility/backends/sequel/column.rb +2 -0
- data/lib/mobility/backends/sequel/container.rb +3 -1
- data/lib/mobility/backends/sequel/hstore.rb +2 -0
- data/lib/mobility/backends/sequel/json.rb +2 -0
- data/lib/mobility/backends/sequel/jsonb.rb +3 -1
- data/lib/mobility/backends/sequel/key_value.rb +8 -6
- data/lib/mobility/backends/sequel/serialized.rb +2 -0
- data/lib/mobility/backends/sequel/table.rb +5 -2
- data/lib/mobility/backends/serialized.rb +1 -3
- data/lib/mobility/backends/table.rb +14 -6
- data/lib/mobility/pluggable.rb +36 -0
- data/lib/mobility/plugin.rb +260 -0
- data/lib/mobility/plugins.rb +26 -25
- data/lib/mobility/plugins/active_model.rb +17 -0
- data/lib/mobility/plugins/active_model/cache.rb +26 -0
- data/lib/mobility/plugins/active_model/dirty.rb +310 -54
- data/lib/mobility/plugins/active_record.rb +34 -0
- data/lib/mobility/plugins/active_record/backend.rb +25 -0
- data/lib/mobility/plugins/active_record/cache.rb +28 -0
- data/lib/mobility/plugins/active_record/dirty.rb +72 -101
- data/lib/mobility/plugins/active_record/query.rb +48 -34
- data/lib/mobility/plugins/active_record/uniqueness_validation.rb +60 -0
- data/lib/mobility/plugins/attribute_methods.rb +28 -20
- data/lib/mobility/plugins/attributes.rb +70 -0
- data/lib/mobility/plugins/backend.rb +138 -0
- data/lib/mobility/plugins/backend_reader.rb +34 -0
- data/lib/mobility/plugins/cache.rb +59 -24
- data/lib/mobility/plugins/default.rb +22 -17
- data/lib/mobility/plugins/dirty.rb +12 -33
- data/lib/mobility/plugins/fallbacks.rb +51 -43
- data/lib/mobility/plugins/fallthrough_accessors.rb +26 -25
- data/lib/mobility/plugins/locale_accessors.rb +25 -35
- data/lib/mobility/plugins/presence.rb +28 -21
- data/lib/mobility/plugins/query.rb +8 -17
- data/lib/mobility/plugins/reader.rb +50 -0
- data/lib/mobility/plugins/sequel.rb +34 -0
- data/lib/mobility/plugins/sequel/backend.rb +25 -0
- data/lib/mobility/plugins/sequel/cache.rb +24 -0
- data/lib/mobility/plugins/sequel/dirty.rb +45 -32
- data/lib/mobility/plugins/sequel/query.rb +21 -6
- data/lib/mobility/plugins/writer.rb +44 -0
- data/lib/mobility/translations.rb +95 -0
- data/lib/mobility/version.rb +12 -1
- data/lib/rails/generators/mobility/templates/initializer.rb +95 -77
- metadata +51 -51
- metadata.gz.sig +0 -0
- data/lib/mobility/active_model.rb +0 -4
- data/lib/mobility/active_model/backend_resetter.rb +0 -26
- data/lib/mobility/active_record.rb +0 -23
- data/lib/mobility/active_record/backend_resetter.rb +0 -26
- data/lib/mobility/active_record/uniqueness_validator.rb +0 -60
- data/lib/mobility/attributes.rb +0 -324
- data/lib/mobility/backend/orm_delegator.rb +0 -44
- data/lib/mobility/backend_resetter.rb +0 -50
- data/lib/mobility/configuration.rb +0 -138
- data/lib/mobility/fallbacks.rb +0 -28
- data/lib/mobility/interface.rb +0 -0
- data/lib/mobility/loaded.rb +0 -4
- data/lib/mobility/plugins/active_record/attribute_methods.rb +0 -38
- data/lib/mobility/plugins/cache/translation_cacher.rb +0 -40
- data/lib/mobility/sequel.rb +0 -9
- data/lib/mobility/sequel/backend_resetter.rb +0 -23
- data/lib/mobility/translates.rb +0 -73
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Mobility
|
4
|
+
module Plugins
|
5
|
+
module Reader
|
6
|
+
=begin
|
7
|
+
|
8
|
+
Defines attribute reader that delegates to +Mobility::Backend#read+.
|
9
|
+
|
10
|
+
=end
|
11
|
+
extend Plugin
|
12
|
+
|
13
|
+
default true
|
14
|
+
requires :backend
|
15
|
+
|
16
|
+
initialize_hook do |*names, **|
|
17
|
+
if options[:reader]
|
18
|
+
names.each do |name|
|
19
|
+
class_eval <<-EOM, __FILE__, __LINE__ + 1
|
20
|
+
def #{name}(locale: nil, **options)
|
21
|
+
#{Reader.setup_source}
|
22
|
+
mobility_backends[:#{name}].read(locale, options)
|
23
|
+
end
|
24
|
+
EOM
|
25
|
+
class_eval <<-EOM, __FILE__, __LINE__ + 1
|
26
|
+
def #{name}?(locale: nil, **options)
|
27
|
+
#{Reader.setup_source}
|
28
|
+
mobility_backends[:#{name}].present?(locale, options)
|
29
|
+
end
|
30
|
+
EOM
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.setup_source
|
36
|
+
<<-EOL
|
37
|
+
return super() if options[:super]
|
38
|
+
if (locale &&= locale.to_sym)
|
39
|
+
#{"Mobility.enforce_available_locales!(locale)" if I18n.enforce_available_locales}
|
40
|
+
options[:locale] = true
|
41
|
+
else
|
42
|
+
locale = Mobility.locale
|
43
|
+
end
|
44
|
+
EOL
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
register_plugin(:reader, Reader)
|
49
|
+
end
|
50
|
+
end
|
@@ -1,6 +1,40 @@
|
|
1
|
+
require "sequel"
|
2
|
+
raise VersionNotSupportedError, "Mobility is only compatible with Sequel 4.0 and greater" if ::Sequel::MAJOR < 4
|
3
|
+
require "sequel/plugins/mobility"
|
4
|
+
unless defined?(ActiveSupport::Inflector)
|
5
|
+
# TODO: avoid automatically including the inflector extension
|
6
|
+
require "sequel/extensions/inflector"
|
7
|
+
end
|
8
|
+
require "sequel/plugins/dirty"
|
9
|
+
require_relative "./sequel/backend"
|
10
|
+
require_relative "./sequel/dirty"
|
11
|
+
require_relative "./sequel/cache"
|
12
|
+
require_relative "./sequel/query"
|
13
|
+
|
1
14
|
module Mobility
|
2
15
|
module Plugins
|
3
16
|
module Sequel
|
17
|
+
extend Plugin
|
18
|
+
|
19
|
+
requires :sequel_backend, include: :after
|
20
|
+
requires :sequel_dirty
|
21
|
+
requires :sequel_cache
|
22
|
+
requires :sequel_query
|
23
|
+
|
24
|
+
included_hook do |klass|
|
25
|
+
unless sequel_class?(klass)
|
26
|
+
name = klass.name || klass.to_s
|
27
|
+
raise TypeError, "#{name} should be a subclass of Sequel::Model to use the sequel plugin"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def sequel_class?(klass)
|
34
|
+
klass < ::Sequel::Model
|
35
|
+
end
|
4
36
|
end
|
37
|
+
|
38
|
+
register_plugin(:sequel, Sequel)
|
5
39
|
end
|
6
40
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Mobility
|
2
|
+
module Plugins
|
3
|
+
module Sequel
|
4
|
+
module Backend
|
5
|
+
extend Plugin
|
6
|
+
|
7
|
+
requires :backend, include: :before
|
8
|
+
|
9
|
+
def load_backend(backend)
|
10
|
+
if Symbol === backend
|
11
|
+
require "mobility/backends/sequel/#{backend}"
|
12
|
+
Backends.load_backend("sequel_#{backend}".to_sym)
|
13
|
+
else
|
14
|
+
super
|
15
|
+
end
|
16
|
+
rescue LoadError => e
|
17
|
+
raise unless e.message =~ /sequel\/#{backend}/
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
register_plugin(:sequel_backend, Sequel::Backend)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Mobility
|
4
|
+
module Plugins
|
5
|
+
module Sequel
|
6
|
+
=begin
|
7
|
+
|
8
|
+
Adds hook to clear Mobility cache when +refresh+ is called on Sequel model.
|
9
|
+
|
10
|
+
=end
|
11
|
+
module Cache
|
12
|
+
extend Plugin
|
13
|
+
|
14
|
+
requires :cache, include: false
|
15
|
+
|
16
|
+
included_hook do |klass|
|
17
|
+
define_cache_hooks(klass, :refresh) if options[:cache]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
register_plugin(:sequel_cache, Sequel::Cache)
|
23
|
+
end
|
24
|
+
end
|
@@ -13,47 +13,60 @@ Automatically includes dirty plugin in model class when enabled.
|
|
13
13
|
=end
|
14
14
|
module Sequel
|
15
15
|
module Dirty
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
16
|
+
extend Plugin
|
17
|
+
|
18
|
+
requires :dirty, include: false
|
19
|
+
|
20
|
+
initialize_hook do
|
21
|
+
# Although we load the plugin in the included callback method, we
|
22
|
+
# need to include this module here in advance to ensure that its
|
23
|
+
# instance methods are included *before* the ones defined here.
|
24
|
+
include ::Sequel::Plugins::Dirty::InstanceMethods
|
25
|
+
end
|
26
|
+
|
27
|
+
included_hook do |klass, backend_class|
|
28
|
+
if options[:dirty]
|
29
|
+
# this just adds Sequel::Plugins::Dirty to @plugins
|
30
|
+
klass.plugin :dirty
|
31
|
+
define_dirty_methods(names)
|
32
|
+
backend_class.include BackendMethods
|
27
33
|
end
|
28
34
|
end
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
%w[initial_value column_change column_changed? reset_column].each do |method_name|
|
41
|
-
define_method method_name do |column|
|
42
|
-
if attribute_names.map(&:to_sym).include?(column)
|
43
|
-
super(Mobility.normalize_locale_accessor(column).to_sym)
|
44
|
-
else
|
45
|
-
super(column)
|
46
|
-
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def define_dirty_methods(names)
|
39
|
+
%w[initial_value column_change column_changed? reset_column].each do |method_name|
|
40
|
+
define_method method_name do |column|
|
41
|
+
if names.map(&:to_sym).include?(column)
|
42
|
+
super(Mobility.normalize_locale_accessor(column).to_sym)
|
43
|
+
else
|
44
|
+
super(column)
|
47
45
|
end
|
48
46
|
end
|
49
47
|
end
|
48
|
+
end
|
50
49
|
|
51
|
-
|
52
|
-
|
53
|
-
|
50
|
+
module BackendMethods
|
51
|
+
# @!group Backend Accessors
|
52
|
+
# @!macro backend_writer
|
53
|
+
# @param [Hash] options
|
54
|
+
def write(locale, value, options = {})
|
55
|
+
locale_accessor = Mobility.normalize_locale_accessor(attribute, locale).to_sym
|
56
|
+
if model.column_changes.has_key?(locale_accessor) && model.initial_values[locale_accessor] == value
|
57
|
+
super
|
58
|
+
[model.changed_columns, model.initial_values].each { |h| h.delete(locale_accessor) }
|
59
|
+
elsif read(locale, options.merge(fallback: false)) != value
|
60
|
+
model.will_change_column(locale_accessor)
|
61
|
+
super
|
62
|
+
end
|
54
63
|
end
|
64
|
+
# @!endgroup
|
55
65
|
end
|
66
|
+
|
56
67
|
end
|
57
68
|
end
|
69
|
+
|
70
|
+
register_plugin(:sequel_dirty, Sequel::Dirty)
|
58
71
|
end
|
59
72
|
end
|
@@ -8,11 +8,18 @@ See ActiveRecord::Query plugin.
|
|
8
8
|
=end
|
9
9
|
module Sequel
|
10
10
|
module Query
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
extend Plugin
|
12
|
+
|
13
|
+
requires :query, include: false
|
14
|
+
|
15
|
+
included_hook do |klass, _|
|
16
|
+
plugin = self
|
17
|
+
if options[:query]
|
18
|
+
raise MissingBackend, "backend required for Query plugin" unless backend_class
|
19
|
+
|
20
|
+
klass.class_eval do
|
14
21
|
extend QueryMethod
|
15
|
-
singleton_class.send :alias_method,
|
22
|
+
singleton_class.send :alias_method, plugin.query_method, :__mobility_query_dataset__
|
16
23
|
end
|
17
24
|
end
|
18
25
|
end
|
@@ -95,7 +102,7 @@ See ActiveRecord::Query plugin.
|
|
95
102
|
|
96
103
|
class << self
|
97
104
|
def build(dataset, query_method, query_conds, &block)
|
98
|
-
return yield unless Hash === query_conds.first
|
105
|
+
return yield unless ::Hash === query_conds.first
|
99
106
|
|
100
107
|
cond = query_conds.first.dup
|
101
108
|
locale = cond.delete(:locale) || Mobility.locale
|
@@ -109,7 +116,7 @@ See ActiveRecord::Query plugin.
|
|
109
116
|
keys, predicates = cond.keys, []
|
110
117
|
model = dataset.model
|
111
118
|
|
112
|
-
query_map = model.
|
119
|
+
query_map = attribute_modules(model).inject(IDENTITY) do |qm, mod|
|
113
120
|
i18n_keys = mod.names.map(&:to_sym) & keys
|
114
121
|
next qm if i18n_keys.empty?
|
115
122
|
|
@@ -127,6 +134,10 @@ See ActiveRecord::Query plugin.
|
|
127
134
|
query_map[dataset.public_send(query_method, ::Sequel.&(*predicates))]
|
128
135
|
end
|
129
136
|
|
137
|
+
def attribute_modules(model)
|
138
|
+
model.ancestors.grep(::Mobility::Translations)
|
139
|
+
end
|
140
|
+
|
130
141
|
def build_predicate(op, values)
|
131
142
|
vals = values.is_a?(Array) ? values.uniq: [values]
|
132
143
|
vals = vals.first if vals.size == 1
|
@@ -135,6 +146,10 @@ See ActiveRecord::Query plugin.
|
|
135
146
|
end
|
136
147
|
end
|
137
148
|
end
|
149
|
+
|
150
|
+
class MissingBackend < Mobility::Error; end
|
138
151
|
end
|
152
|
+
|
153
|
+
register_plugin(:sequel_query, Sequel::Query)
|
139
154
|
end
|
140
155
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Mobility
|
4
|
+
module Plugins
|
5
|
+
module Writer
|
6
|
+
=begin
|
7
|
+
|
8
|
+
Defines attribute writer that delegates to +Mobility::Backend#write+.
|
9
|
+
|
10
|
+
=end
|
11
|
+
extend Plugin
|
12
|
+
|
13
|
+
default true
|
14
|
+
requires :backend
|
15
|
+
|
16
|
+
initialize_hook do |*names|
|
17
|
+
if options[:writer]
|
18
|
+
names.each do |name|
|
19
|
+
class_eval <<-EOM, __FILE__, __LINE__ + 1
|
20
|
+
def #{name}=(value, locale: nil, **options)
|
21
|
+
#{Writer.setup_source}
|
22
|
+
mobility_backends[:#{name}].write(locale, value, options)
|
23
|
+
end
|
24
|
+
EOM
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.setup_source
|
30
|
+
<<-EOL
|
31
|
+
return super(value) if options[:super]
|
32
|
+
if (locale &&= locale.to_sym)
|
33
|
+
#{"Mobility.enforce_available_locales!(locale)" if I18n.enforce_available_locales}
|
34
|
+
options[:locale] = true
|
35
|
+
else
|
36
|
+
locale = Mobility.locale
|
37
|
+
end
|
38
|
+
EOL
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
register_plugin(:writer, Writer)
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "mobility/pluggable"
|
3
|
+
|
4
|
+
module Mobility
|
5
|
+
=begin
|
6
|
+
|
7
|
+
Module containing translation accessor methods and other methods for accessing
|
8
|
+
translations.
|
9
|
+
|
10
|
+
Normally this class will be created by calling +translates+ on the model class,
|
11
|
+
added when extending {Mobility}, but it can also be used independent of that
|
12
|
+
macro.
|
13
|
+
|
14
|
+
==Including Translations in a Class
|
15
|
+
|
16
|
+
Since {Translations} is a subclass of +Module+, including an instance of it is
|
17
|
+
like including a module. Options passed to the initializer are passed to
|
18
|
+
plugins (if those plugins have been enabled).
|
19
|
+
|
20
|
+
We can first enable plugins by subclassing `Mobility::Translations`, and
|
21
|
+
calling +plugins+ to enable any plugins we want to use. We want reader and
|
22
|
+
writer methods for accessing translations, so we enable those plugins:
|
23
|
+
|
24
|
+
class Translations < Mobility::Translations
|
25
|
+
plugins do
|
26
|
+
reader
|
27
|
+
writer
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
Both `reader` and `writer` depend on the `backend` plugin, so this is also enabled.
|
32
|
+
|
33
|
+
Then create an instance like this:
|
34
|
+
|
35
|
+
Translations.new("title", backend: :my_backend)
|
36
|
+
|
37
|
+
This will generate an anonymous module that behaves approximately like this:
|
38
|
+
|
39
|
+
Module.new do
|
40
|
+
# From Mobility::Plugins::Backend module
|
41
|
+
#
|
42
|
+
def mobility_backends
|
43
|
+
# Returns a memoized hash with attribute name keys and backend instance
|
44
|
+
# values. When a key is fetched from the hash, the hash calls
|
45
|
+
# +self.class.mobility_backend_class(name)+ (where +name+ is the
|
46
|
+
# attribute name) to get the backend class, then instantiate it (passing
|
47
|
+
# the model instance and attribute name to its initializer) and return it.
|
48
|
+
end
|
49
|
+
|
50
|
+
# From Mobility::Plugins::Reader module
|
51
|
+
#
|
52
|
+
def title(locale: Mobility.locale)
|
53
|
+
mobility_backends[:title].read(locale)
|
54
|
+
end
|
55
|
+
|
56
|
+
def title?(locale: Mobility.locale)
|
57
|
+
mobility_backends[:title].read(locale).present?
|
58
|
+
end
|
59
|
+
|
60
|
+
# From Mobility::Plugins::Writer module
|
61
|
+
def title=(value, locale: Mobility.locale)
|
62
|
+
mobility_backends[:title].write(locale, value)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
Including this module into a model class will thus add the backends method and
|
67
|
+
reader and writer methods for accessing translations. Other plugins (e.g.
|
68
|
+
fallbacks, cache) modify the result returned by the backend, by hooking into
|
69
|
+
the +included+ callback method on the module, see {Mobility::Plugin} for
|
70
|
+
details.
|
71
|
+
|
72
|
+
==Setting up the Model Class
|
73
|
+
|
74
|
+
Accessor methods alone are of limited use without a hook to actually modify the
|
75
|
+
model class. This hook is provided by the {Backend::Setup#setup_model} method,
|
76
|
+
which is added to every backend class when it includes the {Backend} module.
|
77
|
+
|
78
|
+
Assuming the backend has defined a setup block by calling +setup+, this block
|
79
|
+
will be called when {Translations} is {#included} in the model class, passed
|
80
|
+
attributes and options defined when the backend was defined on the model class.
|
81
|
+
This allows a backend to do things like (for example) define associations on a
|
82
|
+
model class required by the backend, as happens in the {Backends::KeyValue} and
|
83
|
+
{Backends::Table} backends.
|
84
|
+
|
85
|
+
Since setup blocks are evaluated on the model class, it is possible that
|
86
|
+
backends can conflict (for example, overwriting previously defined methods).
|
87
|
+
Care should be taken to avoid defining methods on the model class, or where
|
88
|
+
necessary, ensure that names are defined in such a way as to avoid conflicts
|
89
|
+
with other backends.
|
90
|
+
|
91
|
+
=end
|
92
|
+
class Translations < Pluggable
|
93
|
+
include Plugins.load_plugin(:attributes)
|
94
|
+
end
|
95
|
+
end
|
data/lib/mobility/version.rb
CHANGED
@@ -1,5 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Mobility
|
4
|
-
|
4
|
+
def self.gem_version
|
5
|
+
Gem::Version.new VERSION::STRING
|
6
|
+
end
|
7
|
+
|
8
|
+
module VERSION
|
9
|
+
MAJOR = 1
|
10
|
+
MINOR = 0
|
11
|
+
TINY = 0
|
12
|
+
PRE = "alpha"
|
13
|
+
|
14
|
+
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
|
15
|
+
end
|
5
16
|
end
|