dev_suite 0.2.10 → 0.2.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +49 -7
  3. data/Gemfile +4 -0
  4. data/Gemfile.lock +14 -1
  5. data/README.md +4 -0
  6. data/examples/setup.rb +3 -0
  7. data/examples/workflow/conditional_workflow.rb +1 -1
  8. data/examples/workflow/full_workflow.rb +2 -2
  9. data/examples/workflow/loop_workflow.rb +1 -1
  10. data/examples/workflow/order_processing_workflow.rb +13 -11
  11. data/lib/dev_suite/method_tracer/logger.rb +52 -21
  12. data/lib/dev_suite/utils/color/colorizer.rb +1 -1
  13. data/lib/dev_suite/utils/color/palette/default.rb +33 -0
  14. data/lib/dev_suite/utils/construct/component/manager.rb +6 -1
  15. data/lib/dev_suite/utils/data/path_access/data_traverser.rb +86 -0
  16. data/lib/dev_suite/utils/data/path_access/errors.rb +12 -0
  17. data/lib/dev_suite/utils/data/path_access/key_handler.rb +27 -0
  18. data/lib/dev_suite/utils/data/path_access/path_accessor.rb +36 -0
  19. data/lib/dev_suite/utils/data/path_access/path_parser.rb +35 -0
  20. data/lib/dev_suite/utils/data/path_access.rb +5 -151
  21. data/lib/dev_suite/utils/file_writer/file_writer.rb +14 -4
  22. data/lib/dev_suite/utils/file_writer/writer/base.rb +40 -15
  23. data/lib/dev_suite/utils/file_writer/writer/json.rb +1 -1
  24. data/lib/dev_suite/utils/file_writer/writer/text.rb +11 -9
  25. data/lib/dev_suite/utils/file_writer/writer/yaml.rb +1 -1
  26. data/lib/dev_suite/utils/file_writer/writer_manager.rb +40 -17
  27. data/lib/dev_suite/utils/logger/base.rb +54 -0
  28. data/lib/dev_suite/utils/logger/emoji.rb +17 -0
  29. data/lib/dev_suite/utils/logger/errors.rb +9 -0
  30. data/lib/dev_suite/utils/logger/formatter.rb +41 -0
  31. data/lib/dev_suite/utils/logger/logger.rb +29 -0
  32. data/lib/dev_suite/utils/logger.rb +1 -52
  33. data/lib/dev_suite/utils/store/store.rb +3 -1
  34. data/lib/dev_suite/utils/table/renderer/simple.rb +5 -5
  35. data/lib/dev_suite/version.rb +1 -1
  36. metadata +13 -2
@@ -4,167 +4,21 @@ module DevSuite
4
4
  module Utils
5
5
  module Data
6
6
  module PathAccess
7
+ require_relative "path_access/path_accessor"
8
+
7
9
  # Fetch value from a nested structure using a path
8
10
  def get_value_by_path(data, path)
9
- keys = parse_path(path)
10
- traverse_path_for_get(data, keys)
11
+ PathAccessor.get(data, path)
11
12
  end
12
13
 
13
14
  # Set value in a nested structure using a path
14
15
  def set_value_by_path(data, path, value)
15
- keys = parse_path(path)
16
- last_key = keys.pop
17
- target = traverse_path_for_set(data, keys)
18
-
19
- set_final_value(target, last_key, value)
16
+ PathAccessor.set(data, path, value)
20
17
  end
21
18
 
22
19
  # Delete a key from a nested structure using a path
23
20
  def delete_key_by_path(data, path)
24
- keys = parse_path(path)
25
- last_key = keys.pop
26
- target = traverse_path_for_delete(data, keys)
27
-
28
- delete_final_key(target, last_key)
29
- end
30
-
31
- private
32
-
33
- # Parse the path into an array of keys/symbols/integers
34
- def parse_path(path)
35
- return path if path.is_a?(Array)
36
- return [path] if single_symbol_path?(path)
37
-
38
- parse_symbol_or_string_path(path)
39
- end
40
-
41
- # Check if the path is a symbol without dots
42
- def single_symbol_path?(path)
43
- path.is_a?(Symbol) && !path.to_s.include?(".")
44
- end
45
-
46
- # Parse a symbol or string path into an array of keys
47
- def parse_symbol_or_string_path(path)
48
- if path.is_a?(Symbol)
49
- parse_symbol_path(path)
50
- else
51
- parse_string_path(path)
52
- end
53
- end
54
-
55
- # Parse a symbol path that contains dots (e.g., :"test.1")
56
- def parse_symbol_path(path)
57
- path.to_s.split(".").map { |part| parse_part(part) }
58
- end
59
-
60
- # Parse a string path into keys (e.g., 'users.1.name')
61
- def parse_string_path(path)
62
- path.to_s.split(/\.|\[|\]/).reject(&:empty?).map { |part| parse_part(part) }
63
- end
64
-
65
- # Parse each part into either a symbol or integer
66
- def parse_part(part)
67
- part.match?(/^\d+$/) ? part.to_i : part.to_sym
68
- end
69
-
70
- # Helper to traverse the path for getting values
71
- def traverse_path_for_get(data, keys)
72
- keys.reduce(data) do |current_data, key|
73
- check_invalid_path(current_data, key)
74
-
75
- case current_data
76
- when Hash
77
- fetch_from_hash(current_data, key)
78
- when Array
79
- fetch_from_array(current_data, key)
80
- else
81
- raise KeyError, "Invalid data type at '#{key}'"
82
- end
83
- end
84
- end
85
-
86
- # Helper to traverse the path for setting values
87
- def traverse_path_for_set(data, keys)
88
- keys.reduce(data) do |current_data, key|
89
- check_invalid_path(current_data, key)
90
-
91
- case current_data
92
- when Hash
93
- current_data[find_existing_key(current_data, key)] ||= {}
94
- when Array
95
- current_data[key.to_i] ||= {}
96
- else
97
- break nil
98
- end
99
- end
100
- end
101
-
102
- # Helper to check for invalid paths
103
- def check_invalid_path(data, key)
104
- raise KeyError, "Invalid path at '#{key}'" if data.nil?
105
- end
106
-
107
- # Fetch value from a hash, trying both symbol and string keys
108
- def fetch_from_hash(hash, key)
109
- hash[key.to_sym] || hash[key.to_s]
110
- end
111
-
112
- # Fetch value from an array if the key is an integer
113
- def fetch_from_array(array, key)
114
- key.is_a?(Integer) ? array[key] : nil
115
- end
116
-
117
- # Set the final value in a hash or array
118
- def set_final_value(target, last_key, value)
119
- case target
120
- when Hash
121
- target[find_existing_key(target, last_key)] = value
122
- when Array
123
- if last_key.is_a?(Integer)
124
- target[last_key] = value
125
- else
126
- raise KeyError, "Invalid path or type at '#{last_key}'"
127
- end
128
- else
129
- raise KeyError, "Invalid target type for path at '#{last_key}'"
130
- end
131
- end
132
-
133
- # Check if the key already exists and return the key in its original type (symbol or string)
134
- def find_existing_key(hash, key)
135
- return key if hash.key?(key) # Key already exists in original form
136
- return key.to_s if hash.key?(key.to_s) # Exists as a string
137
- return key.to_sym if hash.key?(key.to_sym) # Exists as a symbol
138
-
139
- key # Otherwise, return the key as-is (use the incoming type)
140
- end
141
-
142
- # Helper to traverse the path for deletion
143
- def traverse_path_for_delete(data, keys)
144
- keys.reduce(data) do |current_data, key|
145
- check_invalid_path(current_data, key)
146
-
147
- case current_data
148
- when Hash
149
- current_data[find_existing_key(current_data, key)]
150
- when Array
151
- current_data[key.to_i]
152
- else
153
- raise KeyError, "Invalid data type at '#{key}'"
154
- end
155
- end
156
- end
157
-
158
- # Helper to delete the final key in a hash or array
159
- def delete_final_key(target, last_key)
160
- case target
161
- when Hash
162
- target.delete(find_existing_key(target, last_key))
163
- when Array
164
- target.delete_at(last_key.to_i) if last_key.is_a?(Integer)
165
- else
166
- raise KeyError, "Cannot delete key from unsupported data type"
167
- end
21
+ PathAccessor.delete(data, path)
168
22
  end
169
23
  end
170
24
  end
@@ -10,13 +10,23 @@ module DevSuite
10
10
 
11
11
  class << self
12
12
  def write(path, content)
13
- writer = WriterManager.new
14
- writer.write(path, content)
13
+ WriterManager.write(path, content)
14
+ end
15
+
16
+ def append(path, content)
17
+ WriterManager.append(path, content)
18
+ end
19
+
20
+ def delete_lines(path, start_line, end_line = start_line)
21
+ WriterManager.delete_lines(path, start_line, end_line)
15
22
  end
16
23
 
17
24
  def update_key(path, key, value)
18
- writer = WriterManager.new
19
- writer.update_key(path, key, value)
25
+ WriterManager.update_key(path, key, value)
26
+ end
27
+
28
+ def delete_key(path, key)
29
+ WriterManager.delete_key(path, key)
20
30
  end
21
31
  end
22
32
  end
@@ -5,21 +5,51 @@ module DevSuite
5
5
  module FileWriter
6
6
  module Writer
7
7
  class Base < Utils::Construct::Component::Base
8
- # Abstract method that subclasses must implement for custom file write operations
9
- def write(_path, _content)
10
- raise NotImplementedError, "Subclasses must implement the `write` method"
8
+ attr_reader :path
9
+
10
+ def initialize(path)
11
+ super()
12
+ @path = path
13
+ end
14
+
15
+ def read
16
+ FileLoader.load(path)
11
17
  end
12
18
 
13
- # General method to update a key in any structured file
14
- def update_key(path, key, value, **options)
15
- # Step 1: Load the existing content of the file (this will be subclass-specific)
16
- content = load_file_content(path)
19
+ def write(content, backup: false)
20
+ create_backup if backup
21
+ AtomicWriter.new(path, content).write
22
+ end
17
23
 
18
- # Step 2: Use a utility function to modify the content at the specified key path
24
+ def append(content)
25
+ current_content = read
26
+ updated_content = current_content.merge(content)
27
+ write(updated_content)
28
+ end
29
+
30
+ def delete_lines(start_line, end_line = start_line)
31
+ lines = ::File.readlines(path)
32
+ lines.slice!(start_line - 1, end_line - start_line + 1)
33
+ write(lines.join)
34
+ end
35
+
36
+ def update_key(key, value, **options)
37
+ content = read
19
38
  Utils::Data.set_value_by_path(content, key, value)
39
+ write(content, **options)
40
+ end
41
+
42
+ def delete_key(key, **options)
43
+ content = read
44
+ Utils::Data.delete_key_by_path(content, key)
45
+ write(content, **options)
46
+ end
20
47
 
21
- # Step 3: Write the updated content back to the file using the subclass's write method
22
- write(path, content, **options)
48
+ def append_array(array_key, new_elements)
49
+ data = read
50
+ data[array_key] ||= []
51
+ data[array_key].concat(new_elements)
52
+ write(data)
23
53
  end
24
54
 
25
55
  private
@@ -28,11 +58,6 @@ module DevSuite
28
58
  ::File.exist?(path)
29
59
  end
30
60
 
31
- # Load the file content using a file loader
32
- def load_file_content(file_path)
33
- FileLoader.load(file_path)
34
- end
35
-
36
61
  def create_backup(path)
37
62
  BackupManager.new(path).create_backup if file_exists?(path)
38
63
  end
@@ -5,7 +5,7 @@ module DevSuite
5
5
  module FileWriter
6
6
  module Writer
7
7
  class Json < Base
8
- def write(path, content, pretty: false, backup: false)
8
+ def write(content, pretty: false, backup: false)
9
9
  create_backup(path) if backup
10
10
 
11
11
  json_content = convert_to_json(content, pretty)
@@ -5,20 +5,22 @@ module DevSuite
5
5
  module FileWriter
6
6
  module Writer
7
7
  class Text < Base
8
- def write(path, content, backup: false)
9
- create_backup(path) if backup
10
-
11
- AtomicWriter.new(path, content).write
12
- end
13
-
14
8
  # Updates a key-like pattern in a plain text file (find and replace)
15
- def update_key(path, key, value, backup: false)
16
- content = load_file_content(path)
9
+ def update_key(key, value, backup: false)
10
+ content = read
17
11
 
18
12
  # Simple pattern matching and replacement (e.g., `key: value`)
19
13
  updated_content = content.gsub(/^#{Regexp.escape(key)}:.*/, "#{key}: #{value}")
20
14
 
21
- write(path, updated_content, backup: backup)
15
+ write(updated_content, backup: backup)
16
+ end
17
+
18
+ def append(content)
19
+ ::File.open(path, "a") do |file|
20
+ file.write("\n") if ::File.size(path).nonzero?
21
+
22
+ file.write(content.strip)
23
+ end
22
24
  end
23
25
  end
24
26
  end
@@ -5,7 +5,7 @@ module DevSuite
5
5
  module FileWriter
6
6
  module Writer
7
7
  class Yaml < Base
8
- def write(path, content, normalize: false, backup: false, yaml_options: {})
8
+ def write(content, normalize: false, backup: false, yaml_options: {})
9
9
  validate_content(content)
10
10
  create_backup(path) if backup
11
11
 
@@ -3,27 +3,50 @@
3
3
  module DevSuite
4
4
  module Utils
5
5
  module FileWriter
6
+ class UnsupportedFileTypeError < StandardError; end
7
+
6
8
  class WriterManager
7
- def write(path, content)
8
- writer = writer_for(path)
9
- writer.write(path, content)
10
- end
9
+ WRITERS = {
10
+ ".json" => Writer::Json,
11
+ ".yml" => Writer::Yaml,
12
+ ".yaml" => Writer::Yaml,
13
+ ".txt" => Writer::Text,
14
+ }.freeze
11
15
 
12
- def update_key(path, key, value)
13
- writer = writer_for(path)
14
- writer.update_key(path, key, value)
15
- end
16
+ class << self
17
+ def write(path, content)
18
+ writer_instance(path).write(content)
19
+ end
20
+
21
+ def append(path, content)
22
+ writer_instance(path).append(content)
23
+ end
24
+
25
+ def delete_lines(path, start_line, end_line = start_line)
26
+ writer_instance(path).delete_lines(start_line, end_line)
27
+ end
16
28
 
17
- private
29
+ def update_key(path, key, value)
30
+ writer_instance(path).update_key(key, value)
31
+ end
32
+
33
+ def delete_key(path, key)
34
+ writer_instance(path).delete_key(key)
35
+ end
36
+
37
+ private
38
+
39
+ # Returns the appropriate writer instance based on the file extension
40
+ def writer_instance(path)
41
+ writer_class = WRITERS[file_extension(path)]
42
+ raise UnsupportedFileTypeError, "Unsupported file type: #{file_extension(path)}" unless writer_class
43
+
44
+ writer_class.new(path)
45
+ end
18
46
 
19
- def writer_for(path)
20
- case ::File.extname(path)
21
- when ".json"
22
- Writer::Json.new
23
- when ".yml", ".yaml"
24
- Writer::Yaml.new
25
- else
26
- Writer::Text.new
47
+ # Returns the file extension from the path
48
+ def file_extension(path)
49
+ ::File.extname(path).downcase
27
50
  end
28
51
  end
29
52
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DevSuite
4
+ module Utils
5
+ module Logger
6
+ require_relative "formatter"
7
+ require_relative "emoji"
8
+ require_relative "errors"
9
+
10
+ class Base
11
+ LOG_LEVELS = [:none, :info, :warn, :error, :debug].freeze
12
+
13
+ attr_accessor :log_level
14
+
15
+ def initialize(log_level: :none)
16
+ validate_log_level(log_level)
17
+ @log_level = log_level
18
+ end
19
+
20
+ # Logs a message with optional emoji, prefix, and color.
21
+ def log(message, level: :none, emoji: nil, prefix: nil, color: nil)
22
+ validate_log_level(level)
23
+ return if skip_logging?(level)
24
+
25
+ options = {
26
+ level: level,
27
+ emoji: emoji,
28
+ prefix: prefix,
29
+ color: color,
30
+ }
31
+
32
+ formatted_message = Formatter.format(message, options)
33
+ output_log(formatted_message)
34
+ end
35
+
36
+ private
37
+
38
+ def validate_log_level(level)
39
+ return if LOG_LEVELS.include?(level)
40
+
41
+ raise InvalidLogLevelError, "Invalid log level: #{level}. Valid levels are: #{LOG_LEVELS.join(", ")}."
42
+ end
43
+
44
+ def skip_logging?(level)
45
+ LOG_LEVELS.index(level) < LOG_LEVELS.index(@log_level)
46
+ end
47
+
48
+ def output_log(formatted_message)
49
+ puts formatted_message if formatted_message
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DevSuite
4
+ module Utils
5
+ module Logger
6
+ module Emoji
7
+ class << self
8
+ def resolve(emoji)
9
+ return "" unless emoji
10
+
11
+ emoji.is_a?(Symbol) ? Utils::Emoji.get(emoji) : emoji
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DevSuite
4
+ module Utils
5
+ module Logger
6
+ class InvalidLogLevelError < StandardError; end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DevSuite
4
+ module Utils
5
+ module Logger
6
+ module Formatter
7
+ LOG_DETAILS = {
8
+ none: { prefix: "", color: :white },
9
+ info: { prefix: "[INFO]", color: :green },
10
+ warn: { prefix: "[WARNING]", color: :yellow },
11
+ error: { prefix: "[ERROR]", color: :red },
12
+ debug: { prefix: "[DEBUG]", color: :blue },
13
+ }.freeze
14
+
15
+ class << self
16
+ def format(message, options = {})
17
+ return "" if message.nil? || message.strip.empty?
18
+
19
+ details = fetch_log_details(options[:level])
20
+ prefix = options[:prefix] || details[:prefix]
21
+ color = options[:color] || details[:color]
22
+ emoji_icon = Emoji.resolve(options[:emoji])
23
+
24
+ formatted_message = build_message(prefix, emoji_icon, message)
25
+ Utils::Color.colorize(formatted_message, color: color)
26
+ end
27
+
28
+ private
29
+
30
+ def fetch_log_details(level)
31
+ LOG_DETAILS[level || :none]
32
+ end
33
+
34
+ def build_message(prefix, emoji_icon, message)
35
+ "#{prefix} #{emoji_icon} #{message}".strip
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DevSuite
4
+ module Utils
5
+ module Logger
6
+ require_relative "base"
7
+
8
+ class << self
9
+ # Provides access to the global logger instance
10
+ #
11
+ # @return [Base] the global logger instance
12
+ def logger
13
+ @logger ||= Base.new
14
+ end
15
+
16
+ # Logs a message using the global logger instance
17
+ #
18
+ # @param message [String] The message to log.
19
+ # @param level [Symbol] The log level (:none, :info, :warn, :error, :debug).
20
+ # @param emoji [String, Symbol, nil] Optional emoji to prepend to the message.
21
+ # @param prefix [String, nil] Custom prefix for the message.
22
+ # @param color [Symbol, nil] Custom color for the message.
23
+ def log(message, level: :none, emoji: nil, prefix: nil, color: nil)
24
+ logger.log(message, level: level, emoji: emoji, prefix: prefix, color: color)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -3,58 +3,7 @@
3
3
  module DevSuite
4
4
  module Utils
5
5
  module Logger
6
- LOG_DETAILS = {
7
- none: { prefix: "", color: :white },
8
- info: { prefix: "[INFO]", color: :green },
9
- warn: { prefix: "[WARNING]", color: :yellow },
10
- error: { prefix: "[ERROR]", color: :red },
11
- debug: { prefix: "[DEBUG]", color: :blue },
12
- }.freeze
13
-
14
- class << self
15
- # Logs a message with an optional emoji and specified log level.
16
- #
17
- # @param message [String] The message to log.
18
- # @param level [Symbol] The log level (:info, :warn, :error, :debug).
19
- # @param emoji [String, Symbol] Optional emoji to prepend to the message.
20
- def log(message, level: :none, emoji: nil, prefix: nil, color: nil)
21
- emoji_icon = resolve_emoji(emoji)
22
- formatted_message = format_message("#{emoji_icon} #{message}", level, prefix, color)
23
- output_log(formatted_message)
24
- end
25
-
26
- private
27
-
28
- # Resolves the emoji, either from a symbol or directly as a string.
29
- #
30
- # @param emoji [String, Symbol, nil] The emoji or its symbol key.
31
- # @return [String] The resolved emoji or an empty string if none is provided.
32
- def resolve_emoji(emoji)
33
- return "" unless emoji
34
-
35
- emoji.is_a?(Symbol) ? Emoji.get(emoji) : emoji
36
- end
37
-
38
- # Formats the log message with the appropriate prefix and color.
39
- #
40
- # @param message [String] The message to format.
41
- # @param level [Symbol] The log level (:info, :warn, :error, :debug).
42
- # @return [String] The formatted log message.
43
- def format_message(message, level, custom_prefix, custom_color)
44
- details = LOG_DETAILS[level]
45
- prefix = custom_prefix || details[:prefix]
46
- color = custom_color || details[:color]
47
-
48
- Utils::Color.colorize("#{prefix} #{message}", color: color)
49
- end
50
-
51
- # Outputs the formatted log message to the console.
52
- #
53
- # @param formatted_message [String] The message to output.
54
- def output_log(formatted_message)
55
- puts formatted_message if formatted_message
56
- end
57
- end
6
+ require_relative "logger/logger"
58
7
  end
59
8
  end
60
9
  end
@@ -44,7 +44,9 @@ module DevSuite
44
44
  end
45
45
 
46
46
  def build_driver(options)
47
- Driver.build_component(options.fetch(:driver), **options.except(:driver))
47
+ driver_type = options.fetch(:driver)
48
+ driver_options = options.reject { |key, _| key == :driver }
49
+ Driver.build_component(driver_type, **driver_options)
48
50
  end
49
51
  end
50
52
 
@@ -49,7 +49,7 @@ module DevSuite
49
49
 
50
50
  total_width = column_widths.sum + column_widths.size * 3 - 1
51
51
  title_str = "| #{table.title.center(total_width - 2)} |"
52
- colorize(title_str, settings.get("colors.title"))
52
+ puts colorize(title_str, settings.get("colors.title"))
53
53
  end
54
54
 
55
55
  def render_header(table, column_widths)
@@ -59,13 +59,13 @@ module DevSuite
59
59
  text_align(column.name, column_widths[index])
60
60
  end
61
61
  header_str = "| #{header.join(" | ")} |"
62
- colorize(header_str, settings.get("colors.column"))
62
+ puts colorize(header_str, settings.get("colors.column"))
63
63
  end
64
64
 
65
65
  def render_separator(column_widths)
66
66
  separator = column_widths.map { |width| "-" * width }.join("-+-")
67
67
  separator_str = "+-#{separator}-+"
68
- colorize(separator_str, settings.get("colors.border"))
68
+ puts colorize(separator_str, settings.get("colors.border"))
69
69
  end
70
70
 
71
71
  def render_rows(table, column_widths)
@@ -73,7 +73,7 @@ module DevSuite
73
73
  render_row(row, column_widths)
74
74
  end
75
75
  cells_str = cells.join("\n")
76
- colorize(cells_str, settings.get("colors.row"))
76
+ puts colorize(cells_str, settings.get("colors.row"))
77
77
  end
78
78
 
79
79
  def render_row(row, column_widths)
@@ -81,7 +81,7 @@ module DevSuite
81
81
  text_align(cell.to_s, column_widths[index])
82
82
  end
83
83
  cell_str = "| #{cell.join(" | ")} |"
84
- colorize(cell_str, settings.get("colors.row"))
84
+ puts colorize(cell_str, settings.get("colors.row"))
85
85
  end
86
86
  end
87
87
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DevSuite
4
- VERSION = "0.2.10"
4
+ VERSION = "0.2.12"
5
5
  end