rbs_activerecord 1.2.1 → 1.4.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/hooks/claude-code-web-session-start.sh +14 -0
  3. data/.claude/hooks/pre-commit-check.sh +73 -0
  4. data/.claude/hooks/protect-sig-files.sh +45 -0
  5. data/.claude/hooks/rbs-inline.sh +95 -0
  6. data/.claude/hooks/self-review.sh +93 -0
  7. data/.claude/settings.json +58 -0
  8. data/.rubocop.yml +61 -1
  9. data/.vscode/settings.json +16 -1
  10. data/README.md +7 -0
  11. data/Rakefile +32 -3
  12. data/lib/generators/rbs_activerecord/install_generator.rb +5 -2
  13. data/lib/rbs_activerecord/generator/active_storage/instance_methods.rb +3 -2
  14. data/lib/rbs_activerecord/generator/active_storage/scopes.rb +1 -1
  15. data/lib/rbs_activerecord/generator/associations.rb +4 -4
  16. data/lib/rbs_activerecord/generator/attributes.rb +30 -5
  17. data/lib/rbs_activerecord/generator/delegated_type/instance_methods.rb +5 -5
  18. data/lib/rbs_activerecord/generator/delegated_type/scopes.rb +3 -3
  19. data/lib/rbs_activerecord/generator/enum/base.rb +2 -2
  20. data/lib/rbs_activerecord/generator/enum/instance_methods.rb +3 -3
  21. data/lib/rbs_activerecord/generator/enum/mappings.rb +2 -2
  22. data/lib/rbs_activerecord/generator/enum/scopes.rb +2 -2
  23. data/lib/rbs_activerecord/generator/scopes.rb +2 -2
  24. data/lib/rbs_activerecord/generator/secure_password.rb +1 -1
  25. data/lib/rbs_activerecord/generator.rb +6 -3
  26. data/lib/rbs_activerecord/model.rb +2 -1
  27. data/lib/rbs_activerecord/parser/evaluator.rb +3 -12
  28. data/lib/rbs_activerecord/parser/include_expander/module.rb +7 -5
  29. data/lib/rbs_activerecord/parser/include_expander.rb +4 -4
  30. data/lib/rbs_activerecord/parser/visitor.rb +1 -1
  31. data/lib/rbs_activerecord/parser.rb +2 -1
  32. data/lib/rbs_activerecord/rake_task.rb +6 -3
  33. data/lib/rbs_activerecord/utils.rb +8 -8
  34. data/lib/rbs_activerecord/version.rb +1 -1
  35. data/rbs_collection.lock.yaml +136 -64
  36. data/rbs_collection.yaml +1 -0
  37. data/sig/generators/rbs_activerecord/install_generator.rbs +1 -1
  38. data/sig/rbs_activerecord/generator/active_storage/instance_methods.rbs +1 -0
  39. data/sig/rbs_activerecord/generator/attributes.rbs +14 -1
  40. data/sig/rbs_activerecord/generator.rbs +4 -1
  41. data/sig/rbs_activerecord/parser.rbs +2 -1
  42. data/sig/rbs_activerecord/rake_task.rbs +4 -2
  43. data/sig/rbs_activerecord/utils.rbs +2 -2
  44. metadata +11 -7
@@ -6,16 +6,19 @@ module RbsActiverecord
6
6
  include Utils
7
7
 
8
8
  attr_reader :model #: RbsActiverecord::Model
9
+ attr_reader :pure_accessors #: bool
9
10
 
10
11
  # @rbs model: RbsActiverecord::Model
11
- def initialize(model) #: void
12
+ # @rbs pure_accessors: bool
13
+ def initialize(model, pure_accessors:) #: void
12
14
  @model = model
15
+ @pure_accessors = pure_accessors
13
16
  end
14
17
 
15
18
  def generate #: String
16
19
  <<~RBS
17
20
  module GeneratedAttributeMethods
18
- #{model.columns.map { |c| column(c) }.join("\n")}
21
+ #{model.columns.map { column(_1) }.join("\n")}
19
22
  #{attributes.map { |name, type| attribute(name, type) }.join("\n")}
20
23
  #{model.attribute_aliases.map { |from, to| alias_method(from, to) }.join("\n")}
21
24
  end
@@ -24,14 +27,17 @@ module RbsActiverecord
24
27
 
25
28
  private
26
29
 
30
+ # @rbs col: untyped
27
31
  def column(col) #: String # rubocop:disable Metrics/AbcSize
28
32
  type = sql_type_to_class(col.type)
29
33
  optional = "#{type}?"
30
34
  column_type = col.null ? optional : type
35
+ writer_type = writer_type_for(col.type, nullable: col.null)
31
36
 
32
37
  <<~RBS
38
+ #{"%a{pure}" if pure_accessors}
33
39
  def #{col.name}: () -> #{column_type}
34
- def #{col.name}=: (#{column_type}) -> #{column_type}
40
+ def #{col.name}=: (#{writer_type}) -> #{writer_type}
35
41
  def #{col.name}?: () -> bool
36
42
  def #{col.name}_changed?: () -> bool
37
43
  def #{col.name}_change: () -> [#{optional}, #{optional}]
@@ -53,7 +59,7 @@ module RbsActiverecord
53
59
 
54
60
  def attributes #: Hash[String, untyped]
55
61
  model.attribute_types.filter_map do |name, type|
56
- [name, type] if model.columns.none? { |col| col.name == name }
62
+ [name, type] if model.columns.none? { _1.name == name }
57
63
  end.to_h
58
64
  end
59
65
 
@@ -61,13 +67,32 @@ module RbsActiverecord
61
67
  # @rbs type: untyped
62
68
  def attribute(name, type) #: String
63
69
  column_type = sql_type_to_class(type.type)
70
+ writer_type = writer_type_for(type.type, nullable: false)
64
71
 
65
72
  <<~RBS
66
73
  def #{name}: () -> #{column_type}
67
- def #{name}=: (#{column_type}) -> #{column_type}
74
+ def #{name}=: (#{writer_type}) -> #{writer_type}
68
75
  RBS
69
76
  end
70
77
 
78
+ # The type accepted by the attribute writer.
79
+ # It is wider than the reader type for the types ActiveRecord casts on assignment.
80
+ #
81
+ # @rbs type: Symbol
82
+ # @rbs nullable: bool
83
+ def writer_type_for(type, nullable:) #: String
84
+ writer_type = case type
85
+ when :datetime
86
+ # A datetime attribute also accepts ::Time and converts it to ::ActiveSupport::TimeWithZone
87
+ "(::ActiveSupport::TimeWithZone | ::Time)"
88
+ else
89
+ sql_type_to_class(type)
90
+ end
91
+ nullable ? "#{writer_type}?" : writer_type
92
+ end
93
+
94
+ # @rbs from: untyped
95
+ # @rbs to: untyped
71
96
  def alias_method(from, to) #: String
72
97
  <<~RBS
73
98
  alias #{from} #{to}
@@ -19,7 +19,7 @@ module RbsActiverecord
19
19
  def generate #: String
20
20
  <<~RBS.strip
21
21
  module GeneratedDelegatedTypeInstanceMethods
22
- #{delegated_types.map { |node| delegated_type(node) }.join("\n")}
22
+ #{delegated_types.map { delegated_type(_1) }.join("\n")}
23
23
  end
24
24
  RBS
25
25
  end
@@ -27,18 +27,18 @@ module RbsActiverecord
27
27
  private
28
28
 
29
29
  def delegated_types #: Array[Prism::CallNode]
30
- declarations.select { |node| node.name == :delegated_type }
30
+ declarations.select { _1.name == :delegated_type }
31
31
  end
32
32
 
33
33
  # @rbs node: Prism::CallNode
34
- def delegated_type(node) #: String # rubocop:disable Metrics/AbcSize
34
+ def delegated_type(node) #: String
35
35
  arguments = node.arguments&.arguments || []
36
- name, options = arguments.map { |arg| Parser.eval_node(arg) } #: [String?, Hash[Symbol, untyped]]
36
+ name, options = arguments.map { Parser.eval_node(_1) } #: [String?, Hash[Symbol, untyped]]
37
37
  return "" unless name
38
38
 
39
39
  types = options[:types]
40
40
  role_methods = <<~RBS
41
- def #{name}_class: () -> (#{types.map { |type| "::#{type}" }.join(" | ")})
41
+ def #{name}_class: () -> (#{types.map { "::#{_1}" }.join(" | ")})
42
42
  def #{name}_name: () -> ::String
43
43
  RBS
44
44
 
@@ -17,7 +17,7 @@ module RbsActiverecord
17
17
  def generate #: String
18
18
  <<~RBS.strip
19
19
  module GeneratedDelegatedTypeScopeMethods[Relation]
20
- #{delegated_types.map { |node| delegated_type(node) }.join("\n")}
20
+ #{delegated_types.map { delegated_type(_1) }.join("\n")}
21
21
  end
22
22
  RBS
23
23
  end
@@ -25,13 +25,13 @@ module RbsActiverecord
25
25
  private
26
26
 
27
27
  def delegated_types #: Array[Prism::CallNode]
28
- declarations.select { |node| node.name == :delegated_type }
28
+ declarations.select { _1.name == :delegated_type }
29
29
  end
30
30
 
31
31
  # @rbs node: Prism::CallNode
32
32
  def delegated_type(node) #: String
33
33
  arguments = node.arguments&.arguments || []
34
- name, options = arguments.map { |arg| Parser.eval_node(arg) } #: [String?, Hash[Symbol, untyped]]
34
+ name, options = arguments.map { Parser.eval_node(_1) } #: [String?, Hash[Symbol, untyped]]
35
35
  return "" unless name
36
36
 
37
37
  options[:types].map do |type|
@@ -7,9 +7,9 @@ module RbsActiverecord
7
7
  private
8
8
 
9
9
  # @rbs node: Prism::CallNode
10
- def parse_arguments(node) #: [String?, Array[String | Symbol], Hash[Symbol, untyped]] # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
10
+ def parse_arguments(node) #: [String?, Array[String | Symbol], Hash[Symbol, untyped]] # rubocop:disable Metrics/CyclomaticComplexity
11
11
  arguments = node.arguments&.arguments || []
12
- args = arguments.map { |arg| Parser.eval_node(arg) }
12
+ args = arguments.map { Parser.eval_node(_1) }
13
13
  return nil, [], {} if args.empty?
14
14
 
15
15
  name = args[0] #: String?
@@ -21,7 +21,7 @@ module RbsActiverecord
21
21
  def generate #: String
22
22
  <<~RBS.strip
23
23
  module GeneratedEnumInstanceMethods
24
- #{enums.map { |node| enum(node) }.join("\n")}
24
+ #{enums.map { enum(_1) }.join("\n")}
25
25
  end
26
26
  RBS
27
27
  end
@@ -29,7 +29,7 @@ module RbsActiverecord
29
29
  private
30
30
 
31
31
  def enums #: Array[Prism::CallNode]
32
- declarations.select { |node| node.name == :enum }
32
+ declarations.select { _1.name == :enum }
33
33
  end
34
34
 
35
35
  # @rbs node: Prism::CallNode
@@ -58,7 +58,7 @@ module RbsActiverecord
58
58
 
59
59
  # @rbs name: String
60
60
  def column_type(name) #: String
61
- col = model.columns.find { |c| c.name == name.to_s }
61
+ col = model.columns.find { _1.name == name.to_s }
62
62
  if col
63
63
  sql_type_to_class(col.type)
64
64
  else
@@ -19,7 +19,7 @@ module RbsActiverecord
19
19
  def generate #: String
20
20
  <<~RBS.strip
21
21
  module GeneratedEnumMappingMethods
22
- #{enums.map { |node| enum(node) }.join("\n")}
22
+ #{enums.map { enum(_1) }.join("\n")}
23
23
  end
24
24
  RBS
25
25
  end
@@ -27,7 +27,7 @@ module RbsActiverecord
27
27
  private
28
28
 
29
29
  def enums #: Array[Prism::CallNode]
30
- declarations.select { |node| node.name == :enum }
30
+ declarations.select { _1.name == :enum }
31
31
  end
32
32
 
33
33
  # @rbs node: Prism::CallNode
@@ -19,7 +19,7 @@ module RbsActiverecord
19
19
  def generate #: String
20
20
  <<~RBS.strip
21
21
  module GeneratedEnumScopeMethods[Relation]
22
- #{enums.map { |node| enum(node) }.join("\n")}
22
+ #{enums.map { enum(_1) }.join("\n")}
23
23
  end
24
24
  RBS
25
25
  end
@@ -27,7 +27,7 @@ module RbsActiverecord
27
27
  private
28
28
 
29
29
  def enums #: Array[Prism::CallNode]
30
- declarations.select { |node| node.name == :enum }
30
+ declarations.select { _1.name == :enum }
31
31
  end
32
32
 
33
33
  # @rbs node: Prism::CallNode
@@ -16,7 +16,7 @@ module RbsActiverecord
16
16
  def generate #: String
17
17
  <<~RBS.strip
18
18
  module GeneratedScopeMethods[Relation]
19
- #{scopes.map { |node| scope(node) }.join("\n")}
19
+ #{scopes.map { scope(_1) }.join("\n")}
20
20
  end
21
21
  RBS
22
22
  end
@@ -24,7 +24,7 @@ module RbsActiverecord
24
24
  private
25
25
 
26
26
  def scopes #: Array[Prism::CallNode]
27
- declarations.select { |node| node.name == :scope }
27
+ declarations.select { _1.name == :scope }
28
28
  end
29
29
 
30
30
  # @rbs node: Prism::CallNode
@@ -40,7 +40,7 @@ module RbsActiverecord
40
40
  return [] unless secure_password?
41
41
 
42
42
  model.klass.instance_methods.grep(/authenticate_/).filter_map do |method|
43
- next if model.klass.instance_methods.include?(:"#{method}_confirmation")
43
+ next if model.klass.method_defined?(:"#{method}_confirmation")
44
44
 
45
45
  method.to_s.split("_", 2).last
46
46
  end
@@ -3,18 +3,21 @@
3
3
  require "rails"
4
4
 
5
5
  module RbsActiverecord
6
- class Generator # rubocop:disable Metrics/ClassLength
6
+ class Generator
7
7
  include Utils
8
8
 
9
9
  attr_reader :klass #: singleton(ActiveRecord::Base)
10
10
  attr_reader :klass_name #: String
11
11
  attr_reader :model #: RbsActiverecord::Model
12
+ attr_reader :pure_accessors #: bool
12
13
 
13
14
  # @rbs klass: singleton(ActiveRecord::Base)
14
- def initialize(klass) #: void
15
+ # @rbs pure_accessors: bool
16
+ def initialize(klass, pure_accessors: false) #: void
15
17
  @klass = klass
16
18
  @klass_name = klass.name || ""
17
19
  @model = Model.new(klass)
20
+ @pure_accessors = pure_accessors
18
21
  end
19
22
 
20
23
  def generate #: String # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
@@ -22,7 +25,7 @@ module RbsActiverecord
22
25
  # resolve-type-names: false
23
26
 
24
27
  #{header}
25
- #{Attributes.new(model).generate}
28
+ #{Attributes.new(model, pure_accessors:).generate}
26
29
  #{Associations.new(model).generate}
27
30
 
28
31
  #{ActiveStorage::InstanceMethods.new(model).generate}
@@ -14,6 +14,7 @@ module RbsActiverecord
14
14
  # def columns: () -> Array[untyped]
15
15
  # def defined_enums: () -> Hash[String, untyped]
16
16
  # def reflect_on_all_associations: (Symbol) -> Array[untyped]
17
+
17
18
  def_delegators :klass, :attribute_aliases, :attribute_types, :columns, :defined_enums, :reflect_on_all_associations
18
19
 
19
20
  # @rbs klass: singleton(ActiveRecord::Base)
@@ -27,7 +28,7 @@ module RbsActiverecord
27
28
 
28
29
  def parents #: Array[singleton(ActiveRecord::Base)]
29
30
  ancestors = klass.ancestors #: Array[singleton(ActiveRecord::Base)]
30
- ancestors.select { |cls| cls < ActiveRecord::Base && cls != klass && !cls.abstract_class? }
31
+ ancestors.select { _1 < ActiveRecord::Base && _1 != klass && !_1.abstract_class? }
31
32
  end
32
33
  end
33
34
  end
@@ -4,7 +4,7 @@ module RbsActiverecord
4
4
  module Parser
5
5
  module Evaluator
6
6
  # @rbs node: Prism::Node
7
- def eval_node(node) #: untyped # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
7
+ def eval_node(node) #: untyped # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
8
8
  case node
9
9
  when Prism::NilNode
10
10
  nil
@@ -19,8 +19,8 @@ module RbsActiverecord
19
19
  when Prism::StringNode
20
20
  node.unescaped
21
21
  when Prism::ArrayNode
22
- node.elements.map { |e| eval_node(e) }
23
- when Prism::HashNode
22
+ node.elements.map { eval_node(_1) }
23
+ when Prism::HashNode, Prism::KeywordHashNode
24
24
  node.elements.filter_map do |assoc|
25
25
  case assoc
26
26
  when Prism::AssocNode
@@ -34,15 +34,6 @@ module RbsActiverecord
34
34
  when Prism::ConstantPathNode
35
35
  parent = node.parent ? eval_node(node.parent) : nil
36
36
  "#{parent}::#{node.name}"
37
- when Prism::KeywordHashNode
38
- node.elements.filter_map do |assoc|
39
- case assoc
40
- when Prism::AssocNode
41
- key = eval_node(assoc.key)
42
- value = eval_node(assoc.value)
43
- [key, value]
44
- end
45
- end.to_h
46
37
  end
47
38
  end
48
39
  module_function :eval_node
@@ -21,7 +21,7 @@ module RbsActiverecord
21
21
  def included_blocks #: Array[Prism::CallNode]
22
22
  return [] unless concern?
23
23
 
24
- declarations.select { |node| node.name == :included }
24
+ declarations.select { _1.name == :included }
25
25
  end
26
26
 
27
27
  private
@@ -31,10 +31,12 @@ module RbsActiverecord
31
31
  def declarations #: Array[Prism::CallNode]
32
32
  @declarations ||= begin
33
33
  path = source_location
34
- return [] unless path && File.exist?(path)
35
-
36
- declarations = Parser.parse_file(path)
37
- declarations.fetch(name, [])
34
+ if path && File.exist?(path)
35
+ declarations = Parser.parse_file(path)
36
+ declarations.fetch(name, [])
37
+ else
38
+ []
39
+ end
38
40
  end
39
41
  end
40
42
 
@@ -22,7 +22,7 @@ module RbsActiverecord
22
22
  def expand #: void
23
23
  declarations.each_value do |decls|
24
24
  loop do
25
- index = decls.index { |node| node.name == :include }
25
+ index = decls.index { _1.name == :include }
26
26
  break unless index
27
27
 
28
28
  included_module = decls.delete_at(index) || raise
@@ -39,8 +39,8 @@ module RbsActiverecord
39
39
  # @rbs node: Prism::CallNode
40
40
  def included_blocks_for(node) #: Array[Prism::CallNode]
41
41
  modules = node.arguments&.arguments.to_a
42
- .map { |arg| Parser.eval_node(arg) }
43
- .select { |mod| mod.is_a?(String) }
42
+ .map { Parser.eval_node(_1) }
43
+ .grep(String)
44
44
  modules.flat_map do |modname|
45
45
  mod = const_get(modname)
46
46
  next [] unless mod
@@ -69,7 +69,7 @@ module RbsActiverecord
69
69
  case node.block.body
70
70
  when Prism::StatementsNode
71
71
  body = node.block.body.body #: Array[Prism::CallNode]
72
- body.select { |n| n.is_a?(Prism::CallNode) }
72
+ body.grep(Prism::CallNode)
73
73
  else
74
74
  []
75
75
  end
@@ -14,7 +14,7 @@ module RbsActiverecord
14
14
  end
15
15
 
16
16
  def current_namespace #: String
17
- context.map { |node| Parser.eval_node(node.constant_path) }.join("::")
17
+ context.map { Parser.eval_node(_1.constant_path) }.join("::")
18
18
  end
19
19
 
20
20
  # @rbs override
@@ -14,8 +14,9 @@ module RbsActiverecord
14
14
  end
15
15
  module_function :parse
16
16
 
17
+ # @rbs filename: String
17
18
  def parse_file(filename) #: Hash[String, Array[Prism::CallNode]]
18
- File.open(filename) { |f| parse(f.read) }
19
+ File.open(filename) { parse(_1.read) }
19
20
  end
20
21
  module_function :parse_file
21
22
 
@@ -4,14 +4,17 @@ require "rake/tasklib"
4
4
 
5
5
  module RbsActiverecord
6
6
  class RakeTask < Rake::TaskLib
7
- attr_accessor :name, :signature_root_dir
7
+ attr_accessor :name #: Symbol
8
+ attr_accessor :pure_accessors #: bool
9
+ attr_accessor :signature_root_dir #: Pathname
8
10
 
9
11
  # @rbs name: Symbol
10
12
  # @rbs &block: (RakeTask) -> void
11
- def initialize(name = :'rbs:activerecord', &block) #: void
13
+ def initialize(name = :"rbs:activerecord", &block) #: void
12
14
  super()
13
15
 
14
16
  @name = name
17
+ @pure_accessors = false
15
18
  @signature_root_dir = Rails.root / "sig/activerecord"
16
19
 
17
20
  block&.call(self)
@@ -48,7 +51,7 @@ module RbsActiverecord
48
51
  next if klass.abstract_class?
49
52
  next unless klass.connection.table_exists?(klass.table_name)
50
53
 
51
- rbs = Generator.new(klass).generate
54
+ rbs = Generator.new(klass, pure_accessors:).generate
52
55
 
53
56
  path = signature_root_dir / "app/models/#{klass.name.underscore}.rbs"
54
57
  path.dirname.mkpath
@@ -8,11 +8,11 @@ module RbsActiverecord
8
8
  def format(str) #: String
9
9
  parsed = RBS::Parser.parse_signature(str)
10
10
  StringIO.new.tap do |out|
11
- RBS::Writer.new(out: out).write(parsed[1] + parsed[2])
11
+ RBS::Writer.new(out:).write(parsed[1] + parsed[2])
12
12
  end.string
13
13
  end
14
14
 
15
- # @rbs klass_name: singleton(Object)
15
+ # @rbs klass: singleton(Object)
16
16
  def klass_to_names(klass) #: Array[RBS::TypeName]
17
17
  type_name = RBS::TypeName.parse("::#{klass.name}")
18
18
 
@@ -26,22 +26,22 @@ module RbsActiverecord
26
26
  end
27
27
 
28
28
  # @rbs klass: singleton(ActiveRecord::Base)
29
- def primary_key_type_for(klass) #: String # rubocop:disable Metrics/AbcSize
29
+ def primary_key_type_for(klass) #: String
30
30
  case klass.primary_key
31
31
  when Array
32
32
  primary_keys = klass.primary_key.map(&:to_s)
33
33
  types = klass.columns
34
- .select { |column| primary_keys.include?(column.name) }
35
- .map { |pk| sql_type_to_class(pk.type) }
34
+ .select { primary_keys.include?(_1.name) }
35
+ .map { sql_type_to_class(_1.type) }
36
36
  "[#{types.join(" | ")}]"
37
37
  else
38
- primary_key = klass.columns.find { |column| column.name == klass.primary_key }
38
+ primary_key = klass.columns.find { _1.name == klass.primary_key }
39
39
  sql_type_to_class(primary_key.type)
40
40
  end
41
41
  end
42
42
 
43
43
  # @rbs type: Symbol
44
- def sql_type_to_class(type) #: String # rubocop:disable Metris/CyclomaticComplexity
44
+ def sql_type_to_class(type) #: String # rubocop:disable Metrics/CyclomaticComplexity
45
45
  case type
46
46
  when :integer
47
47
  "::Integer"
@@ -63,7 +63,7 @@ module RbsActiverecord
63
63
  "untyped"
64
64
  when :inet
65
65
  "::IPAddr"
66
- else
66
+ else # rubocop:disable Lint/DuplicateBranch
67
67
  "untyped"
68
68
  end
69
69
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RbsActiverecord
4
- VERSION = "1.2.1"
4
+ VERSION = "1.4.0"
5
5
  end