data_model 0.4.0 → 0.6.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +8 -2
  3. data/.solargraph.yml +22 -0
  4. data/Gemfile.lock +71 -29
  5. data/Rakefile +0 -6
  6. data/Steepfile +27 -0
  7. data/data_model.gemspec +1 -2
  8. data/lib/data_model/boolean.rb +0 -2
  9. data/lib/data_model/builtin/array.rb +32 -25
  10. data/lib/data_model/builtin/big_decimal.rb +15 -14
  11. data/lib/data_model/builtin/boolean.rb +10 -7
  12. data/lib/data_model/builtin/date.rb +15 -12
  13. data/lib/data_model/builtin/float.rb +14 -13
  14. data/lib/data_model/builtin/hash.rb +109 -36
  15. data/lib/data_model/builtin/integer.rb +14 -13
  16. data/lib/data_model/builtin/numeric.rb +35 -0
  17. data/lib/data_model/builtin/object.rb +28 -0
  18. data/lib/data_model/builtin/or.rb +73 -0
  19. data/lib/data_model/builtin/string.rb +15 -16
  20. data/lib/data_model/builtin/symbol.rb +14 -13
  21. data/lib/data_model/builtin/time.rb +17 -14
  22. data/lib/data_model/builtin.rb +9 -9
  23. data/lib/data_model/error.rb +30 -18
  24. data/lib/data_model/errors.rb +79 -55
  25. data/lib/data_model/fixtures/array.rb +22 -9
  26. data/lib/data_model/fixtures/big_decimal.rb +9 -7
  27. data/lib/data_model/fixtures/boolean.rb +5 -5
  28. data/lib/data_model/fixtures/date.rb +13 -11
  29. data/lib/data_model/fixtures/example.rb +7 -7
  30. data/lib/data_model/fixtures/float.rb +9 -7
  31. data/lib/data_model/fixtures/hash.rb +22 -10
  32. data/lib/data_model/fixtures/integer.rb +9 -7
  33. data/lib/data_model/fixtures/numeric.rb +31 -0
  34. data/lib/data_model/fixtures/object.rb +27 -0
  35. data/lib/data_model/fixtures/or.rb +29 -0
  36. data/lib/data_model/fixtures/string.rb +15 -32
  37. data/lib/data_model/fixtures/symbol.rb +9 -7
  38. data/lib/data_model/fixtures/time.rb +13 -11
  39. data/lib/data_model/logging.rb +5 -8
  40. data/lib/data_model/model.rb +11 -8
  41. data/lib/data_model/registry.rb +37 -22
  42. data/lib/data_model/scanner.rb +23 -28
  43. data/lib/data_model/struct.rb +116 -0
  44. data/lib/data_model/testing/minitest.rb +33 -9
  45. data/lib/data_model/testing.rb +0 -2
  46. data/lib/data_model/type.rb +38 -22
  47. data/lib/data_model/version.rb +1 -3
  48. data/lib/data_model.rb +8 -17
  49. metadata +13 -26
  50. data/sorbet/config +0 -4
  51. data/sorbet/rbi/annotations/rainbow.rbi +0 -269
  52. data/sorbet/rbi/gems/minitest@5.18.0.rbi +0 -1491
  53. data/sorbet/rbi/gems/zeitwerk.rbi +0 -196
  54. data/sorbet/rbi/gems/zeitwerk@2.6.7.rbi +0 -966
  55. data/sorbet/rbi/todo.rbi +0 -5
  56. data/sorbet/tapioca/config.yml +0 -13
  57. data/sorbet/tapioca/require.rb +0 -4
@@ -1,12 +1,11 @@
1
- # typed: strict
2
-
3
1
  module DataModel
2
+ # Test data for float schemas
4
3
  module Fixtures::Float
5
4
  include Fixtures
6
- extend T::Sig
7
5
  extend self
8
6
 
9
- sig { returns(Example) }
7
+ # a simple float example
8
+ # @return [Example] the example
10
9
  def simple
11
10
  Example.new(
12
11
  [:float],
@@ -18,7 +17,8 @@ module DataModel
18
17
  )
19
18
  end
20
19
 
21
- sig { returns(Example) }
20
+ # a float example that is optional
21
+ # @return [Example] the example
22
22
  def optional
23
23
  Example.new(
24
24
  [:float, { optional: true }],
@@ -28,7 +28,8 @@ module DataModel
28
28
  )
29
29
  end
30
30
 
31
- sig { returns(Example) }
31
+ # a float example where the minimum value is 5
32
+ # @return [Example] the example
32
33
  def min
33
34
  Example.new(
34
35
  [:float, { min: 5 }],
@@ -39,7 +40,8 @@ module DataModel
39
40
  )
40
41
  end
41
42
 
42
- sig { returns(Example) }
43
+ # a float example where the maximum value is 5
44
+ # @return [Example] the example
43
45
  def max
44
46
  Example.new(
45
47
  [:float, { max: 5.0 }],
@@ -1,14 +1,11 @@
1
- # typed: strict
2
-
3
1
  module DataModel
2
+ # Test data for hash schemas
4
3
  module Fixtures::Hash
5
4
  include Fixtures
6
- extend T::Sig
7
5
  extend self
8
6
 
9
- TContact = T.type_alias { T::Hash[Symbol, T.untyped] }
10
-
11
- sig { returns(TContact) }
7
+ # hash data conforming to the contact schema
8
+ # @return [Hash{Symbol => String}] the hash
12
9
  def example_contact
13
10
  {
14
11
  first_name: "foo",
@@ -17,7 +14,20 @@ module DataModel
17
14
  }
18
15
  end
19
16
 
20
- sig { returns(Example) }
17
+ # alternate hash syntax for when you want to type keys and values
18
+ # @return [Example] the example
19
+ def dictionary
20
+ Example.new(
21
+ [:hash, [symbol: :string]],
22
+ variants: {
23
+ valid: { foo: "bar" },
24
+ invalid: { foo: 123 }
25
+ },
26
+ )
27
+ end
28
+
29
+ # hash contact example
30
+ # @return [Example] the example
21
31
  def contact
22
32
  Example.new(
23
33
  [:hash,
@@ -28,14 +38,15 @@ module DataModel
28
38
  valid: example_contact,
29
39
  missing: nil,
30
40
  coercible: example_contact.to_a,
31
- missing_email: example_contact.tap { |h| T.cast(h, TContact).delete(:email) },
41
+ missing_email: example_contact.tap { |h| h.delete(:email) },
32
42
  invalid_field: example_contact.merge(email: 123),
33
43
  other_type: []
34
44
  },
35
45
  )
36
46
  end
37
47
 
38
- sig { returns(Example) }
48
+ # hash contact example that is optional
49
+ # @return [Example] the example
39
50
  def optional_contact
40
51
  Example.new(
41
52
  [:hash, { optional: true },
@@ -49,7 +60,8 @@ module DataModel
49
60
  )
50
61
  end
51
62
 
52
- sig { returns(Example) }
63
+ # hash contact example that is closed to extra keys
64
+ # @return [Example] the example
53
65
  def closed_contact
54
66
  Example.new(
55
67
  [:hash, { open: false },
@@ -1,12 +1,11 @@
1
- # typed: strict
2
-
3
1
  module DataModel
2
+ # Test data for integer schemas
4
3
  module Fixtures::Integer
5
4
  include Fixtures
6
- extend T::Sig
7
5
  extend self
8
6
 
9
- sig { returns(Example) }
7
+ # simple integer example
8
+ # @return [Hash{Symbol => untyped}] the variants used by each example
10
9
  def simple
11
10
  Example.new(
12
11
  [:integer],
@@ -18,7 +17,8 @@ module DataModel
18
17
  )
19
18
  end
20
19
 
21
- sig { returns(Example) }
20
+ # integer example that is optional
21
+ # @return [Example] the example
22
22
  def optional
23
23
  Example.new(
24
24
  [:integer, { optional: true }],
@@ -28,7 +28,8 @@ module DataModel
28
28
  )
29
29
  end
30
30
 
31
- sig { returns(Example) }
31
+ # integer example that has a minimum value
32
+ # @return [Example] the example
32
33
  def min
33
34
  Example.new(
34
35
  [:integer, { min: 5 }],
@@ -39,7 +40,8 @@ module DataModel
39
40
  )
40
41
  end
41
42
 
42
- sig { returns(Example) }
43
+ # integer example that has a maximum value
44
+ # @return [Example] the example
43
45
  def max
44
46
  Example.new(
45
47
  [:integer, { max: 5 }],
@@ -0,0 +1,31 @@
1
+ require "bigdecimal/util"
2
+
3
+ module DataModel
4
+ # test fixtures for object type
5
+ module Fixtures::Numeric
6
+ extend self
7
+ include Fixtures
8
+
9
+ def variants
10
+ {
11
+ missing: nil,
12
+ integer: 1,
13
+ float: 1.0,
14
+ decimal: 1.0.to_d,
15
+ string: ["1", 1]
16
+ }
17
+ end
18
+
19
+ # a simple numeric example
20
+ # @return [Example] the example
21
+ def simple
22
+ Example.new([:numeric], variants:)
23
+ end
24
+
25
+ # a numeric example that is optional
26
+ # @return [Example] the example
27
+ def optional
28
+ Example.new([:numeric, { optional: true }], variants:)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,27 @@
1
+ module DataModel
2
+ # test fixtures for object type
3
+ module Fixtures::Object
4
+ extend self
5
+ include Fixtures
6
+
7
+ def variants
8
+ {
9
+ missing: nil,
10
+ integer: 1,
11
+ string: "string"
12
+ }
13
+ end
14
+
15
+ # a simple object example
16
+ # @return [Example] the example
17
+ def simple
18
+ Example.new([:object], variants:)
19
+ end
20
+
21
+ # a object example that is optional
22
+ # @return [Example] the example
23
+ def optional
24
+ Example.new([:object, { optional: true }], variants:)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,29 @@
1
+ module DataModel
2
+ # test fixtures for object type
3
+ module Fixtures::Or
4
+ extend self
5
+ include Fixtures
6
+
7
+ def variants
8
+ {
9
+ missing: nil,
10
+ integer: 1,
11
+ int_array: [1],
12
+ string: ["1", 1],
13
+ float: 1.0
14
+ }
15
+ end
16
+
17
+ # a simple numeric example, integer or integer array
18
+ # @return [Example] the example
19
+ def simple
20
+ Example.new([:or, :integer, [:array, :integer]], variants:)
21
+ end
22
+
23
+ # a numeric example that is optional
24
+ # @return [Example] the example
25
+ def optional
26
+ Example.new([:or, { optional: true }, :integer, [:array, :integer]], variants:)
27
+ end
28
+ end
29
+ end
@@ -1,12 +1,11 @@
1
- # typed: strict
2
-
3
1
  module DataModel
2
+ # Test data for string schemas
4
3
  module Fixtures::String
5
4
  extend self
6
- extend T::Sig
7
5
  include Fixtures
8
6
 
9
- sig { returns(Example) }
7
+ # a simple string example
8
+ # @return [Example] the example
10
9
  def simple
11
10
  Example.new(
12
11
  [:string],
@@ -18,7 +17,8 @@ module DataModel
18
17
  )
19
18
  end
20
19
 
21
- sig { returns(Example) }
20
+ # an email string example
21
+ # @return [Example] the example
22
22
  def email
23
23
  Example.new(
24
24
  [:string, { format: "@" }],
@@ -29,29 +29,8 @@ module DataModel
29
29
  )
30
30
  end
31
31
 
32
- sig { returns(Example) }
33
- def email_regexp
34
- Example.new(
35
- [:string, { format: /@/ }],
36
- variants: {
37
- valid: "foo@bar.com",
38
- invalid: "invalid"
39
- },
40
- )
41
- end
42
-
43
- sig { returns(Example) }
44
- def email_proc
45
- Example.new(
46
- [:string, { format: ->(val) { val.match?(/@/) } }],
47
- variants: {
48
- valid: "foo@bar.com",
49
- invalid: "invalid"
50
- },
51
- )
52
- end
53
-
54
- sig { returns(Example) }
32
+ # a string example that is optional
33
+ # @return [Example] the example
55
34
  def optional
56
35
  Example.new(
57
36
  [:string, { optional: true }],
@@ -63,7 +42,8 @@ module DataModel
63
42
  )
64
43
  end
65
44
 
66
- sig { returns(Example) }
45
+ # a string example where "valid" is the only allowed String
46
+ # @return [Example] the example
67
47
  def inclusion
68
48
  Example.new(
69
49
  [:string, { included: ["valid"] }],
@@ -74,7 +54,8 @@ module DataModel
74
54
  )
75
55
  end
76
56
 
77
- sig { returns(Example) }
57
+ # a string example where "invalid" is the only disallowed String
58
+ # @return [Example] the example
78
59
  def exclusion
79
60
  Example.new(
80
61
  [:string, { excluded: ["invalid"] }],
@@ -85,7 +66,8 @@ module DataModel
85
66
  )
86
67
  end
87
68
 
88
- sig { returns(Example) }
69
+ # a string example where blank strings are allowed
70
+ # @return [Example] the example
89
71
  def allow_blank
90
72
  Example.new(
91
73
  [:string, { allow_blank: true }],
@@ -97,7 +79,8 @@ module DataModel
97
79
  )
98
80
  end
99
81
 
100
- sig { returns(Example) }
82
+ # a string example where blank strings are not allowed
83
+ # @return [Example] the example
101
84
  def dont_allow_blank
102
85
  Example.new(
103
86
  [:string, { allow_blank: false }],
@@ -1,12 +1,11 @@
1
- # typed: strict
2
-
3
1
  module DataModel
2
+ # Test data around symbol schemas
4
3
  module Fixtures::Symbol
5
4
  extend self
6
5
  include Fixtures
7
- extend T::Sig
8
6
 
9
- sig { returns(Example) }
7
+ # a simple symbol example
8
+ # @return [Example] the example
10
9
  def simple
11
10
  Example.new(
12
11
  [:symbol],
@@ -19,7 +18,8 @@ module DataModel
19
18
  )
20
19
  end
21
20
 
22
- sig { returns(Example) }
21
+ # a symbol example that is optional
22
+ # @return [Example] the example
23
23
  def optional
24
24
  Example.new(
25
25
  [:symbol, { optional: true }],
@@ -31,7 +31,8 @@ module DataModel
31
31
  )
32
32
  end
33
33
 
34
- sig { returns(Example) }
34
+ # a symbol example where :valid is the only allowed Symbol
35
+ # @return [Example] the example
35
36
  def inclusion
36
37
  Example.new(
37
38
  [:symbol, { included: [:valid] }],
@@ -42,7 +43,8 @@ module DataModel
42
43
  )
43
44
  end
44
45
 
45
- sig { returns(Example) }
46
+ # a symbol example where :invalid is the only disallowed Symbol
47
+ # @return [Example] the example
46
48
  def exclusion
47
49
  Example.new(
48
50
  [:symbol, { excluded: [:invalid] }],
@@ -1,22 +1,20 @@
1
- # typed: strict
2
-
3
1
  module DataModel
2
+ # Test data around time schemas
4
3
  module Fixtures::Time
5
- extend T::Sig
6
- extend self
7
4
  include Fixtures
5
+ extend self
8
6
 
9
- sig { returns(::Time) }
7
+ # @return [Time] a time that is used by the #earliest example
10
8
  def earliest_time
11
9
  return ::Time.now - 1
12
10
  end
13
11
 
14
- sig { returns(::Time) }
12
+ # @return [Time] a time that is used by the #latest example
15
13
  def latest_time
16
14
  return ::Time.now + 1
17
15
  end
18
16
 
19
- sig { returns(T::Hash[Symbol, Object]) }
17
+ # @return [Hash{Symbol => untyped}] the variants used by each example
20
18
  def variants
21
19
  now = ::Time.now
22
20
 
@@ -30,22 +28,26 @@ module DataModel
30
28
  }
31
29
  end
32
30
 
33
- sig { returns(Example) }
31
+ # A simple time schema
32
+ # @return [Example] the example
34
33
  def simple
35
34
  Example.new([:time], variants:)
36
35
  end
37
36
 
38
- sig { returns(Example) }
37
+ # A time schema that is optional
38
+ # @return [Example] the example
39
39
  def optional
40
40
  Example.new([:time, { optional: true }], variants:)
41
41
  end
42
42
 
43
- sig { returns(Example) }
43
+ # A time schema that has a restriction on the earliest time
44
+ # @return [Example] the example
44
45
  def earliest
45
46
  Example.new([:time, { earliest: earliest_time }], variants:)
46
47
  end
47
48
 
48
- sig { returns(Example) }
49
+ # A time schema that has a restriction on the latest time
50
+ # @return [Example] the example
49
51
  def latest
50
52
  Example.new([:time, { latest: latest_time }], variants:)
51
53
  end
@@ -1,15 +1,12 @@
1
- # typed: strict
2
-
3
1
  require "logger"
4
2
 
5
3
  module DataModel
4
+ # Provides a logger for classes that include it
6
5
  module Logging
7
- extend T::Sig
8
- include Kernel
9
-
10
- sig { returns(Logger) }
6
+ # Get a logger
7
+ # @return [Logger] the logger for this class
11
8
  def log
12
- target = T.let(respond_to?(:name) ? self : self.class, T.any(Class, Module))
9
+ target = respond_to?(:name) ? self : self.class
13
10
 
14
11
  logger = Logger.new(
15
12
  STDERR,
@@ -17,7 +14,7 @@ module DataModel
17
14
  progname: target.name,
18
15
  )
19
16
 
20
- return @log ||= T.let(logger, T.nilable(Logger))
17
+ return @log ||= logger
21
18
  end
22
19
  end
23
20
  end
@@ -1,28 +1,31 @@
1
- # typed: strict
2
-
3
1
  module DataModel
2
+ # A model is a schema and a type. It is the primary interface for interacting
3
+ # with the data_model gem.
4
4
  class Model
5
- extend T::Sig
6
-
7
- sig { params(schema: TSchema, type: Type).void }
5
+ # Create a new model.
6
+ # @param schema [Array] the schema to define
7
+ # @param type [Type] the type to use
8
+ # @return [void]
8
9
  def initialize(schema, type)
9
10
  @schema = schema
10
11
  @type = type
11
12
  end
12
13
 
13
- sig { returns(TSchema) }
14
+ # @return [Array] the schema configured
14
15
  attr_reader :schema
15
16
 
16
17
  # Validate data against the model. This will return true if the data is valid,
17
18
  # or false if it is not. If it is not valid, it will raise an exception.
18
- sig { params(data: TData).returns(Error) }
19
+ # @param data [Hash] the data to validate
20
+ # @return [Boolean] true if the data is valid, false if it is not
19
21
  def validate(data)
20
22
  _, err = @type.read(data)
21
23
  return err
22
24
  end
23
25
 
24
26
  # Read data with the model. This will return a tuple of [data, error]
25
- sig { params(data: TData).returns([TData, Error]) }
27
+ # @param data [Hash] the data to read
28
+ # @return [Array] a tuple of [data, error]
26
29
  def coerce(data)
27
30
  result = @type.read(data, coerce: true)
28
31
  return result
@@ -1,61 +1,71 @@
1
- # typed: strict
2
-
3
1
  module DataModel
4
2
  # Registry allows for different type implementations to be used by the scanner.
5
3
  # It also acts as an error message registry, mostly for pragmatic reasons.
6
4
  class Registry
7
- extend T::Sig
8
-
9
5
  # Default types that will be used if alternative type map is not given
10
- sig { returns(TTypeMap) }
6
+ # @return [Hash] the default type map
11
7
  def self.default_types
12
8
  Builtin.types
13
9
  end
14
10
 
15
- sig { returns(Errors::TErrorMessages) }
11
+ # Default error messages that will be used if alternative error messages are not given
12
+ # @return [Hash] the default error messages
16
13
  def self.default_error_messages
17
14
  Errors.error_messages
18
15
  end
19
16
 
20
17
  # Singleton instance that will be used globally unless instances given
21
- sig { params(types: TTypeMap, errors: Errors::TErrorMessages).returns(Registry) }
18
+ # @param types [Hash] the type map to use
19
+ # @param errors [Hash] the error message map to use
20
+ # @return [Registry] the singleton instance
22
21
  def self.instance(types: default_types, errors: default_error_messages)
23
- @instance ||= T.let(new(types:, errors:), T.nilable(Registry))
22
+ @instance ||= new(types:, errors:)
24
23
  end
25
24
 
26
25
  # Register a type on the global instance
27
- sig { params(name: Symbol, type: T.class_of(Type)).void }
26
+ # @param name [Symbol] the name of the type
27
+ # @param type [Type] the type to register
28
+ # @return [void]
28
29
  def self.register(name, type)
29
30
  instance.register(name, type)
30
31
  end
31
32
 
32
33
  # Instanciate a new type registry. Default errors will always be used, but additional
33
34
  # errors can be registered.
34
- sig { params(types: TTypeMap, errors: Errors::TErrorMessages).void }
35
+ # @param types [Hash] the type map to use
36
+ # @param errors [Hash] the error message map to use
37
+ # @return [Registry] the new instance
35
38
  def initialize(types: self.class.default_types, errors: self.class.default_error_messages)
36
- @error_messages = T.let(nil, T.nilable(Errors::TErrorMessages))
39
+ @error_messages = nil
40
+
37
41
  if errors
38
42
  errors.each { |type, builder| register_error_message(type, &builder) }
39
43
  end
40
44
 
41
- @types = T.let({}, TTypeMap)
45
+ @types = {}
42
46
  types.each { |(name, type)| register(name, type) }
43
47
  end
44
48
 
45
49
  # Register a type on this instance
46
- sig { params(name: Symbol, type: T.class_of(Type)).void }
50
+ # @param name [Symbol] the name of the Type
51
+ # @param type [Type] the type to register
52
+ # @return [void]
47
53
  def register(name, type)
48
54
  @types[name] = type
49
55
  end
50
56
 
51
57
  # Check if a type is registered
52
- sig { params(name: Symbol).returns(T::Boolean) }
58
+ # @param name [Symbol] the name of the type
59
+ # @return [Boolean] whether the type is registered
53
60
  def type?(name)
54
61
  @types.key?(name)
55
62
  end
56
63
 
57
64
  # Access and configure registered type
58
- sig { params(name: Symbol, args: Type::TArguments, params: T.nilable(T::Array[Object])).returns(Type) }
65
+ # @param name [Symbol] the name of the Type
66
+ # @param args [Hash] the arguments to pass to the Type
67
+ # @param params [Array] the parameters to configure the Type with
68
+ # @return [Type] the configured type
59
69
  def type(name, args: {}, params: nil)
60
70
  if !type?(name)
61
71
  raise "#{name} is not registered as a type"
@@ -73,26 +83,30 @@ module DataModel
73
83
  ## API
74
84
 
75
85
  # Register a custom error message for use with custom errors
76
- sig { params(type: Symbol, block: Errors::TErrorMessageBuilder).void }
86
+ # @param type [Symbol] the type of error to register
87
+ # @param block [Proc] the block to use to build the error message, shoudl take the error context and return a string
88
+ # @return [void]
77
89
  def register_error_message(type, &block)
78
90
  error_message_builders[type] = block
79
91
  end
80
92
 
81
93
  # Get the error message builders
82
- sig { returns(Errors::TErrorMessages) }
94
+ # @return [Hash] the error message builders
83
95
  def error_message_builders
84
96
  if @error_messages.nil?
85
- @error_messages ||= T.let({}, T.nilable(Errors::TErrorMessages))
97
+ @error_messages ||= {}
86
98
  end
87
99
 
88
100
  @error_messages
89
101
  end
90
102
 
91
103
  # Build the error message for a given error
92
- sig { params(error: TError).returns(String) }
104
+ # @param error [Error] the error to build the message for
105
+ # @return [String] the error message
106
+ # @raise [RuntimeError] if no error message builder is registered for the error type
93
107
  def error_message(error)
94
- type = T.let(error[0], Symbol)
95
- ctx = T.let(error[1], T.untyped)
108
+ type = error[0]
109
+ ctx = error[1]
96
110
 
97
111
  builder = error_message_builders[type]
98
112
 
@@ -104,7 +118,8 @@ module DataModel
104
118
  end
105
119
 
106
120
  # Build error messages from error object
107
- sig { params(error: Error).returns(T::Hash[Symbol, T::Array[String]]) }
121
+ # @param error [Error] the error to build the messages for
122
+ # @return [Hash] the error messages
108
123
  def error_messages(error)
109
124
  error.to_h.transform_values do |error_list|
110
125
  error_list.map { |e| error_message(e) }