waterdrop 2.3.0 → 2.3.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f665d9dff358be819f65b6adb794175af3d0858062daaad60aef05cdd99f8023
4
- data.tar.gz: 72598f0afd73a612c162e3e820d0ab705fbe79563f5637656842b8a3f07e357f
3
+ metadata.gz: b1482f0d2955c43c2ab479372c9cbb4a8c039723121c26b2ff75d0e26700d71e
4
+ data.tar.gz: 1bb3e675c0c632d4940b2b661bdb75a74c44358a7e53fa9dc7051fdad62218b0
5
5
  SHA512:
6
- metadata.gz: 7f43f5cff311886bf7e2ff26b3fac2c9280aa6a9916115ff93ee9b07e1dadf8ab8017d6cfce063db34bd00403cc452942d432f41e538ae0f0c23fb95997b2003
7
- data.tar.gz: 89cba9dfeed151f61f6cc113f6b08ccd0ac365161897a3f938b31002a1933ebc40e3103307742281b7b389bcef0e550b98ada6207ac001e8115b03c0b9efaf90
6
+ metadata.gz: 8921844cc44625916974c63478562b66a7f30a833b6f95e71e8c3448109d673bd3520a8a48bdf0fe7296e5e00b988c15c3dd92cab2e11ee37914f667966a14de
7
+ data.tar.gz: 6c6038c4f9a718d107fd885e66bf29cc3dde09c6f6e0be2ba41651f3494672ddd925e71574f53ec8ec70d4e766024018f73a43a5fadf99402117ac3a0751b0c0
checksums.yaml.gz.sig CHANGED
Binary file
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.1.0
1
+ 3.1.2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # WaterDrop changelog
2
2
 
3
+ ## 2.3.3 (2022-07-18)
4
+ - Replace `dry-validation` with home-brew validation layer and drop direct dependency on `dry-validation`.
5
+ - Remove indirect dependency on dry-configurable from DataDog listener (no changes required).
6
+
7
+ ## 2.3.2 (2022-07-17)
8
+ - Replace `dry-configurable` with home-brew config and drop direct dependency on `dry-configurable`.
9
+
10
+ ## 2.3.1 (2022-06-17)
11
+ - Update rdkafka patches to align with `0.12.0` and `0.11.1` support.
12
+
3
13
  ## 2.3.0 (2022-04-03)
4
14
  - Rename StdoutListener to LoggerListener (#240)
5
15
 
data/Gemfile.lock CHANGED
@@ -1,18 +1,16 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- waterdrop (2.3.0)
4
+ waterdrop (2.3.3)
5
5
  concurrent-ruby (>= 1.1)
6
- dry-configurable (~> 0.13)
7
6
  dry-monitor (~> 0.5)
8
- dry-validation (~> 1.7)
9
7
  rdkafka (>= 0.10)
10
8
  zeitwerk (~> 2.3)
11
9
 
12
10
  GEM
13
11
  remote: https://rubygems.org/
14
12
  specs:
15
- activesupport (7.0.2.3)
13
+ activesupport (7.0.3)
16
14
  concurrent-ruby (~> 1.0, >= 1.0.2)
17
15
  i18n (>= 1.6, < 2)
18
16
  minitest (>= 5.1)
@@ -21,54 +19,28 @@ GEM
21
19
  concurrent-ruby (1.1.10)
22
20
  diff-lcs (1.5.0)
23
21
  docile (1.4.0)
24
- dry-configurable (0.14.0)
22
+ dry-configurable (0.15.0)
25
23
  concurrent-ruby (~> 1.0)
26
24
  dry-core (~> 0.6)
27
- dry-container (0.9.0)
28
- concurrent-ruby (~> 1.0)
29
- dry-configurable (~> 0.13, >= 0.13.0)
30
- dry-core (0.7.1)
25
+ dry-core (0.8.0)
31
26
  concurrent-ruby (~> 1.0)
32
27
  dry-events (0.3.0)
33
28
  concurrent-ruby (~> 1.0)
34
29
  dry-core (~> 0.5, >= 0.5)
35
- dry-inflector (0.2.1)
36
- dry-initializer (3.1.1)
37
- dry-logic (1.2.0)
38
- concurrent-ruby (~> 1.0)
39
- dry-core (~> 0.5, >= 0.5)
40
- dry-monitor (0.5.0)
30
+ dry-monitor (0.6.1)
41
31
  dry-configurable (~> 0.13, >= 0.13.0)
42
32
  dry-core (~> 0.5, >= 0.5)
43
33
  dry-events (~> 0.2)
44
- dry-schema (1.9.1)
45
- concurrent-ruby (~> 1.0)
46
- dry-configurable (~> 0.13, >= 0.13.0)
47
- dry-core (~> 0.5, >= 0.5)
48
- dry-initializer (~> 3.0)
49
- dry-logic (~> 1.0)
50
- dry-types (~> 1.5)
51
- dry-types (1.5.1)
52
- concurrent-ruby (~> 1.0)
53
- dry-container (~> 0.3)
54
- dry-core (~> 0.5, >= 0.5)
55
- dry-inflector (~> 0.1, >= 0.1.2)
56
- dry-logic (~> 1.0, >= 1.0.2)
57
- dry-validation (1.8.0)
58
- concurrent-ruby (~> 1.0)
59
- dry-container (~> 0.7, >= 0.7.1)
60
- dry-core (~> 0.5, >= 0.5)
61
- dry-initializer (~> 3.0)
62
- dry-schema (~> 1.9, >= 1.9.1)
34
+ zeitwerk (~> 2.5)
63
35
  factory_bot (6.2.1)
64
36
  activesupport (>= 5.0.0)
65
37
  ffi (1.15.5)
66
38
  i18n (1.10.0)
67
39
  concurrent-ruby (~> 1.0)
68
40
  mini_portile2 (2.8.0)
69
- minitest (5.15.0)
41
+ minitest (5.16.0)
70
42
  rake (13.0.6)
71
- rdkafka (0.11.1)
43
+ rdkafka (0.12.0)
72
44
  ffi (~> 1.15)
73
45
  mini_portile2 (~> 2.6)
74
46
  rake (> 12)
@@ -93,7 +65,7 @@ GEM
93
65
  simplecov_json_formatter (0.1.4)
94
66
  tzinfo (2.0.4)
95
67
  concurrent-ruby (~> 1.0)
96
- zeitwerk (2.5.4)
68
+ zeitwerk (2.6.0)
97
69
 
98
70
  PLATFORMS
99
71
  arm64-darwin-21
@@ -107,4 +79,4 @@ DEPENDENCIES
107
79
  waterdrop!
108
80
 
109
81
  BUNDLED WITH
110
- 2.3.10
82
+ 2.3.15
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # WaterDrop
2
2
 
3
- **Note**: Documentation presented here refers to WaterDrop `2.0.0`.
3
+ **Note**: Documentation presented here refers to WaterDrop `2.x`.
4
4
 
5
- WaterDrop `2.0` does **not** work with Karafka `1.*` and aims to either work as a standalone producer outside of Karafka `1.*` ecosystem or as a part of soon to be released Karafka `2.0.*`.
5
+ WaterDrop `2.x` does **not** work with Karafka `1.*` and aims to either work as a standalone producer outside of Karafka `1.*` ecosystem or as a part of soon to be released Karafka `2.0.*`.
6
6
 
7
7
  Please refer to [this](https://github.com/karafka/waterdrop/tree/1.4) branch and its documentation for details about WaterDrop `1.*` usage.
8
8
 
data/config/errors.yml CHANGED
@@ -1,7 +1,30 @@
1
1
  en:
2
- dry_validation:
3
- errors:
4
- invalid_key_type: all keys need to be of type String
5
- invalid_value_type: all values need to be of type String
6
- max_payload_size: is more than `max_payload_size` config value
2
+ validations:
3
+ config:
4
+ missing: must be present
5
+ logger_format: must be present
6
+ deliver_format: must be boolean
7
+ id_format: must be a non-empty string
8
+ max_payload_size_format: must be an integer that is equal or bigger than 1
9
+ wait_timeout_format: must be a numeric that is bigger than 0
10
+ max_wait_timeout_format: must be an integer that is equal or bigger than 0
11
+ kafka_format: must be a hash with symbol based keys
7
12
  kafka_key_must_be_a_symbol: All keys under the kafka settings scope need to be symbols
13
+
14
+ message:
15
+ missing: must be present
16
+ partition_format: must be an integer greater or equal to -1
17
+ topic_format: 'does not match the topic allowed format'
18
+ partition_key_format: must be a non-empty string
19
+ timestamp_format: must be either time or integer
20
+ payload_format: must be string
21
+ headers_format: must be a hash
22
+ key_format: must be a non-empty string
23
+ payload_max_size: is more than `max_payload_size` config value
24
+ headers_invalid_key_type: all headers keys need to be of type String
25
+ headers_invalid_value_type: all headers values need to be of type String
26
+
27
+ test:
28
+ missing: must be present
29
+ nested.id_format: 'is invalid'
30
+ nested.id2_format: 'is invalid'
@@ -5,7 +5,7 @@
5
5
  module WaterDrop
6
6
  # Configuration object for setting up all options required by WaterDrop
7
7
  class Config
8
- include Dry::Configurable
8
+ include Configurable
9
9
 
10
10
  # Defaults for kafka settings, that will be overwritten only if not present already
11
11
  KAFKA_DEFAULTS = {
@@ -68,6 +68,8 @@ module WaterDrop
68
68
 
69
69
  ::Rdkafka::Config.logger = config.logger
70
70
  end
71
+
72
+ self
71
73
  end
72
74
 
73
75
  private
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WaterDrop
4
+ module Configurable
5
+ # Single end config value representation
6
+ Leaf = Struct.new(:name, :default, :constructor)
7
+ end
8
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WaterDrop
4
+ module Configurable
5
+ # Single non-leaf node
6
+ # This is a core component for the configurable settings
7
+ #
8
+ # The idea here is simple: we collect settings (leafs) and children (nodes) information and we
9
+ # only compile/initialize the values prior to user running the `#configure` API. This API needs
10
+ # to run prior to using the result stuff even if there is nothing to configure
11
+ class Node
12
+ attr_reader :name, :nestings
13
+
14
+ # We need to be able to redefine children for deep copy
15
+ attr_accessor :children
16
+
17
+ # @param name [Symbol] node name
18
+ # @param nestings [Proc] block for nested settings
19
+ def initialize(name, nestings = ->(_) {})
20
+ @name = name
21
+ @children = []
22
+ @nestings = nestings
23
+ instance_eval(&nestings)
24
+ end
25
+
26
+ # Allows for a single leaf or nested node definition
27
+ #
28
+ # @param name [Symbol] setting or nested node name
29
+ # @param default [Object] default value
30
+ # @param constructor [#call, nil] callable or nil
31
+ # @param block [Proc] block for nested settings
32
+ def setting(name, default: nil, constructor: nil, &block)
33
+ @children << if block
34
+ Node.new(name, block)
35
+ else
36
+ Leaf.new(name, default, constructor)
37
+ end
38
+ end
39
+
40
+ # Allows for the configuration and setup of the settings
41
+ #
42
+ # Compile settings, allow for overrides via yielding
43
+ # @return [Node] returns self after configuration
44
+ def configure
45
+ compile
46
+ yield(self) if block_given?
47
+ self
48
+ end
49
+
50
+ # @return [Hash] frozen config hash representation
51
+ def to_h
52
+ config = {}
53
+
54
+ @children.each do |value|
55
+ config[value.name] = if value.is_a?(Leaf)
56
+ public_send(value.name)
57
+ else
58
+ value.to_h
59
+ end
60
+ end
61
+
62
+ config.freeze
63
+ end
64
+
65
+ # Deep copies all the children nodes to allow us for templates building on a class level and
66
+ # non-side-effect usage on an instance/inherited.
67
+ # @return [Node] duplicated node
68
+ def deep_dup
69
+ dupped = Node.new(name, nestings)
70
+
71
+ dupped.children += children.map do |value|
72
+ value.is_a?(Leaf) ? value.dup : value.deep_dup
73
+ end
74
+
75
+ dupped
76
+ end
77
+
78
+ # Converts the settings definitions into end children
79
+ # @note It runs once, after things are compiled, they will not be recompiled again
80
+ def compile
81
+ @children.each do |value|
82
+ # Do not redefine something that was already set during compilation
83
+ # This will allow us to reconfigure things and skip override with defaults
84
+ next if respond_to?(value.name)
85
+
86
+ singleton_class.attr_accessor value.name
87
+
88
+ initialized = if value.is_a?(Leaf)
89
+ value.constructor ? value.constructor.call(value.default) : value.default
90
+ else
91
+ value.compile
92
+ value
93
+ end
94
+
95
+ public_send("#{value.name}=", initialized)
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WaterDrop
4
+ # A simple dry-configuration API compatible module for defining settings with defaults and a
5
+ # constructor.
6
+ module Configurable
7
+ # A simple settings layer that works similar to dry-configurable
8
+ # It allows us to define settings on a class and per instance level with templating on a class
9
+ # level. It handles inheritance and allows for nested settings.
10
+ #
11
+ # @note The core settings template needs to be defined on a class level
12
+ class << self
13
+ # Sets up all the class methods and inits the core root node.
14
+ # Useful when only per class settings are needed as does not include instance methods
15
+ # @param base [Class] class that we extend
16
+ def extended(base)
17
+ base.extend ClassMethods
18
+ end
19
+
20
+ # Sets up all the class and instance methods and inits the core root node
21
+ #
22
+ # @param base [Class] class to which we want to add configuration
23
+ #
24
+ # Needs to be used when per instance configuration is needed
25
+ def included(base)
26
+ base.include InstanceMethods
27
+ base.extend self
28
+ end
29
+ end
30
+
31
+ # Instance related methods
32
+ module InstanceMethods
33
+ # @return [Node] config root node
34
+ def config
35
+ @config ||= self.class.config.deep_dup
36
+ end
37
+
38
+ # Allows for a per instance configuration (if needed)
39
+ # @param block [Proc] block for configuration
40
+ def configure(&block)
41
+ config.configure(&block)
42
+ end
43
+ end
44
+
45
+ # Class related methods
46
+ module ClassMethods
47
+ # @return [Node] root node for the settings
48
+ def config
49
+ return @config if @config
50
+
51
+ # This will handle inheritance
52
+ @config = if superclass.respond_to?(:config)
53
+ superclass.config.deep_dup
54
+ else
55
+ Node.new(:root)
56
+ end
57
+ end
58
+
59
+ # Allows for a per class configuration (if needed)
60
+ # @param block [Proc] block for configuration
61
+ def configure(&block)
62
+ config.configure(&block)
63
+ end
64
+
65
+ # Pipes the settings setup to the config root node
66
+ def setting(...)
67
+ config.setting(...)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WaterDrop
4
+ module Contractable
5
+ # Base contract for all the contracts that check data format
6
+ #
7
+ # @note This contract does NOT support rules inheritance as it was never needed in Karafka
8
+ class Contract
9
+ extend Configurable
10
+
11
+ # Yaml based error messages data
12
+ setting(:error_messages)
13
+
14
+ # Class level API definitions
15
+ class << self
16
+ # @return [Array<Rule>] all the validation rules defined for a given contract
17
+ attr_reader :rules
18
+
19
+ # Allows for definition of a scope/namespace for nested validations
20
+ #
21
+ # @param path [Symbol] path in the hash for nesting
22
+ # @param block [Proc] nested rule code or more nestings inside
23
+ #
24
+ # @example
25
+ # nested(:key) do
26
+ # required(:inside) { |inside| inside.is_a?(String) }
27
+ # end
28
+ def nested(path, &block)
29
+ init_accu
30
+ @nested << path
31
+ instance_eval(&block)
32
+ @nested.pop
33
+ end
34
+
35
+ # Defines a rule for a required field (required means, that will automatically create an
36
+ # error if missing)
37
+ #
38
+ # @param keys [Array<Symbol>] single or full path
39
+ # @param block [Proc] validation rule
40
+ def required(*keys, &block)
41
+ init_accu
42
+ @rules << Rule.new(@nested + keys, :required, block).freeze
43
+ end
44
+
45
+ # @param keys [Array<Symbol>] single or full path
46
+ # @param block [Proc] validation rule
47
+ def optional(*keys, &block)
48
+ init_accu
49
+ @rules << Rule.new(@nested + keys, :optional, block).freeze
50
+ end
51
+
52
+ # @param block [Proc] validation rule
53
+ #
54
+ # @note Virtual rules have different result expectations. Please see contracts or specs for
55
+ # details.
56
+ def virtual(&block)
57
+ init_accu
58
+ @rules << Rule.new([], :virtual, block).freeze
59
+ end
60
+
61
+ private
62
+
63
+ # Initializes nestings and rules building accumulator
64
+ def init_accu
65
+ @nested ||= []
66
+ @rules ||= []
67
+ end
68
+ end
69
+
70
+ # Runs the validation
71
+ #
72
+ # @param data [Hash] hash with data we want to validate
73
+ # @return [Result] validaton result
74
+ def call(data)
75
+ errors = []
76
+
77
+ self.class.rules.map do |rule|
78
+ case rule.type
79
+ when :required
80
+ validate_required(data, rule, errors)
81
+ when :optional
82
+ validate_optional(data, rule, errors)
83
+ when :virtual
84
+ validate_virtual(data, rule, errors)
85
+ end
86
+ end
87
+
88
+ Result.new(errors, self)
89
+ end
90
+
91
+ # @param data [Hash] data for validation
92
+ # @param error_class [Class] error class that should be used when validation fails
93
+ # @return [Boolean] true
94
+ # @raise [StandardError] any error provided in the error_class that inherits from the
95
+ # standard error
96
+ def validate!(data, error_class)
97
+ result = call(data)
98
+
99
+ return true if result.success?
100
+
101
+ raise error_class, result.errors
102
+ end
103
+
104
+ private
105
+
106
+ # Runs validation for rules on fields that are required and adds errors (if any) to the
107
+ # errors array
108
+ #
109
+ # @param data [Hash] input hash
110
+ # @param rule [Rule] validation rule
111
+ # @param errors [Array] array with errors from previous rules (if any)
112
+ def validate_required(data, rule, errors)
113
+ for_checking = dig(data, rule.path)
114
+
115
+ if for_checking.first == :match
116
+ result = rule.validator.call(for_checking.last, data, errors, self)
117
+
118
+ return if result == true
119
+
120
+ errors << [rule.path, result || :format]
121
+ else
122
+ errors << [rule.path, :missing]
123
+ end
124
+ end
125
+
126
+ # Runs validation for rules on fields that are optional and adds errors (if any) to the
127
+ # errors array
128
+ #
129
+ # @param data [Hash] input hash
130
+ # @param rule [Rule] validation rule
131
+ # @param errors [Array] array with errors from previous rules (if any)
132
+ def validate_optional(data, rule, errors)
133
+ for_checking = dig(data, rule.path)
134
+
135
+ return unless for_checking.first == :match
136
+
137
+ result = rule.validator.call(for_checking.last, data, errors, self)
138
+
139
+ return if result == true
140
+
141
+ errors << [rule.path, result || :format]
142
+ end
143
+
144
+ # Runs validation for rules on virtual fields (aggregates, etc) and adds errors (if any) to
145
+ # the errors array
146
+ #
147
+ # @param data [Hash] input hash
148
+ # @param rule [Rule] validation rule
149
+ # @param errors [Array] array with errors from previous rules (if any)
150
+ def validate_virtual(data, rule, errors)
151
+ result = rule.validator.call(data, errors, self)
152
+
153
+ return if result == true
154
+
155
+ errors.push(*result)
156
+ end
157
+
158
+ # Tries to dig for a given key in a hash and returns it with indication whether or not it was
159
+ # possible to find it (dig returns nil and we don't know if it wasn't the digged key value)
160
+ #
161
+ # @param data [Hash]
162
+ # @param keys [Array<Symbol>]
163
+ # @return [Array<Symbol, Object>] array where the first element is `:match` or `:miss` and
164
+ # the digged value or nil if not found
165
+ def dig(data, keys)
166
+ current = data
167
+ result = :match
168
+
169
+ keys.each do |nesting|
170
+ unless current.key?(nesting)
171
+ result = :miss
172
+
173
+ break
174
+ end
175
+
176
+ current = current[nesting]
177
+ end
178
+
179
+ [result, current]
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WaterDrop
4
+ module Contractable
5
+ # Representation of a validaton result with resolved error messages
6
+ class Result
7
+ attr_reader :errors
8
+
9
+ # Builds a result object and remaps (if needed) error keys to proper error messages
10
+ #
11
+ # @param errors [Array<Array>] array with sub-arrays with paths and error keys
12
+ # @param contract [Object] contract that generated the error
13
+ def initialize(errors, contract)
14
+ # Short track to skip object allocation for the happy path
15
+ if errors.empty?
16
+ @errors = errors
17
+ return
18
+ end
19
+
20
+ hashed = {}
21
+
22
+ errors.each do |error|
23
+ scope = error.first.map(&:to_s).join('.').to_sym
24
+
25
+ # This will allow for usage of custom messages instead of yaml keys if needed
26
+ hashed[scope] = if error.last.is_a?(String)
27
+ error.last
28
+ else
29
+ build_message(contract, scope, error.last)
30
+ end
31
+ end
32
+
33
+ @errors = hashed
34
+ end
35
+
36
+ # @return [Boolean] true if no errors
37
+ def success?
38
+ errors.empty?
39
+ end
40
+
41
+ private
42
+
43
+ # Builds message based on the error messages
44
+ # @param contract [Object] contract for which we build the result
45
+ # @param scope [Symbol] path to the key that has an error
46
+ # @param error_key [Symbol] error key for yaml errors lookup
47
+ # @return [String] error message
48
+ def build_message(contract, scope, error_key)
49
+ messages = contract.class.config.error_messages
50
+
51
+ messages.fetch(error_key.to_s) do
52
+ messages.fetch("#{scope}_#{error_key}")
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WaterDrop
4
+ module Contractable
5
+ # Representation of a single validation rule
6
+ Rule = Struct.new(:path, :type, :validator)
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WaterDrop
4
+ # Contract layer for WaterDrop and Karafka
5
+ # It aims to be "dry-validation" like but smaller and easier to handle + without dependencies
6
+ #
7
+ # It allows for nested validations, etc
8
+ #
9
+ # @note It is thread-safe to run but validations definitions should happen before threads are
10
+ # used.
11
+ module Contractable
12
+ end
13
+ end
@@ -3,27 +3,37 @@
3
3
  module WaterDrop
4
4
  module Contracts
5
5
  # Contract with validation rules for WaterDrop configuration details
6
- class Config < Base
7
- params do
8
- required(:id).filled(:str?)
9
- required(:logger).filled
10
- required(:deliver).filled(:bool?)
11
- required(:max_payload_size).filled(:int?, gteq?: 1)
12
- required(:max_wait_timeout).filled(:number?, gteq?: 0)
13
- required(:wait_timeout).filled(:number?, gt?: 0)
14
- required(:kafka).filled(:hash?)
6
+ class Config < Contractable::Contract
7
+ configure do |config|
8
+ config.error_messages = YAML.safe_load(
9
+ File.read(
10
+ File.join(WaterDrop.gem_root, 'config', 'errors.yml')
11
+ )
12
+ ).fetch('en').fetch('validations').fetch('config')
15
13
  end
16
14
 
15
+ required(:id) { |id| id.is_a?(String) && !id.empty? }
16
+ required(:logger) { |logger| !logger.nil? }
17
+ required(:deliver) { |deliver| [true, false].include?(deliver) }
18
+ required(:max_payload_size) { |ps| ps.is_a?(Integer) && ps >= 1 }
19
+ required(:max_wait_timeout) { |mwt| mwt.is_a?(Numeric) && mwt >= 0 }
20
+ required(:wait_timeout) { |wt| wt.is_a?(Numeric) && wt.positive? }
21
+ required(:kafka) { |kafka| kafka.is_a?(Hash) && !kafka.empty? }
22
+
17
23
  # rdkafka allows both symbols and strings as keys for config but then casts them to strings
18
24
  # This can be confusing, so we expect all keys to be symbolized
19
- rule(:kafka) do
20
- next unless value.is_a?(Hash)
25
+ virtual do |config, errors|
26
+ next true unless errors.empty?
27
+
28
+ errors = []
21
29
 
22
- value.each_key do |key|
23
- next if key.is_a?(Symbol)
30
+ config
31
+ .fetch(:kafka)
32
+ .keys
33
+ .reject { |key| key.is_a?(Symbol) }
34
+ .each { |key| errors << [[:kafka, key], :kafka_key_must_be_a_symbol] }
24
35
 
25
- key(:"kafka.#{key}").failure(:kafka_key_must_be_a_symbol)
26
- end
36
+ errors
27
37
  end
28
38
  end
29
39
  end
@@ -4,36 +4,56 @@ module WaterDrop
4
4
  module Contracts
5
5
  # Contract with validation rules for validating that all the message options that
6
6
  # we provide to producer ale valid and usable
7
- class Message < Base
7
+ class Message < Contractable::Contract
8
+ configure do |config|
9
+ config.error_messages = YAML.safe_load(
10
+ File.read(
11
+ File.join(WaterDrop.gem_root, 'config', 'errors.yml')
12
+ )
13
+ ).fetch('en').fetch('validations').fetch('message')
14
+ end
15
+
8
16
  # Regex to check that topic has a valid format
9
17
  TOPIC_REGEXP = /\A(\w|-|\.)+\z/
10
18
 
11
- # Checks, that the given value is a string
12
- STRING_ASSERTION = ->(value) { value.is_a?(String) }.to_proc
19
+ private_constant :TOPIC_REGEXP
13
20
 
14
- private_constant :TOPIC_REGEXP, :STRING_ASSERTION
21
+ attr_reader :max_payload_size
15
22
 
16
- option :max_payload_size
17
-
18
- params do
19
- required(:topic).filled(:str?, format?: TOPIC_REGEXP)
20
- required(:payload).filled(:str?)
21
- optional(:key).maybe(:str?, :filled?)
22
- optional(:partition).filled(:int?, gteq?: -1)
23
- optional(:partition_key).maybe(:str?, :filled?)
24
- optional(:timestamp).maybe { time? | int? }
25
- optional(:headers).maybe(:hash?)
23
+ # @param max_payload_size [Integer] max payload size
24
+ def initialize(max_payload_size:)
25
+ super()
26
+ @max_payload_size = max_payload_size
26
27
  end
27
28
 
28
- rule(:headers) do
29
- next unless value.is_a?(Hash)
29
+ required(:topic) { |topic| topic.is_a?(String) && TOPIC_REGEXP.match?(topic) }
30
+ required(:payload) { |payload| payload.is_a?(String) }
31
+ optional(:key) { |key| key.nil? || (key.is_a?(String) && !key.empty?) }
32
+ optional(:partition) { |partition| partition.is_a?(Integer) && partition >= -1 }
33
+ optional(:partition_key) { |p_key| p_key.nil? || (p_key.is_a?(String) && !p_key.empty?) }
34
+ optional(:timestamp) { |ts| ts.nil? || (ts.is_a?(Time) || ts.is_a?(Integer)) }
35
+ optional(:headers) { |headers| headers.nil? || headers.is_a?(Hash) }
36
+
37
+ virtual do |config, errors|
38
+ next true unless errors.empty?
39
+ next true unless config.key?(:headers)
40
+ next true if config[:headers].nil?
30
41
 
31
- key.failure(:invalid_key_type) unless value.keys.all?(&STRING_ASSERTION)
32
- key.failure(:invalid_value_type) unless value.values.all?(&STRING_ASSERTION)
42
+ errors = []
43
+
44
+ config.fetch(:headers).each do |key, value|
45
+ errors << [%i[headers], :invalid_key_type] unless key.is_a?(String)
46
+ errors << [%i[headers], :invalid_value_type] unless value.is_a?(String)
47
+ end
48
+
49
+ errors
33
50
  end
34
51
 
35
- rule(:payload) do
36
- key.failure(:max_payload_size) if value.bytesize > max_payload_size
52
+ virtual do |config, errors, validator|
53
+ next true unless errors.empty?
54
+ next true if config[:payload].bytesize <= validator.max_payload_size
55
+
56
+ [[%i[payload], :max_size]]
37
57
  end
38
58
  end
39
59
  end
@@ -11,26 +11,29 @@ module WaterDrop
11
11
  #
12
12
  # @note You need to setup the `dogstatsd-ruby` client and assign it
13
13
  class Listener
14
- include Dry::Configurable
14
+ include WaterDrop::Configurable
15
+ extend Forwardable
16
+
17
+ def_delegators :config, :client, :rd_kafka_metrics, :namespace, :default_tags
15
18
 
16
19
  # Value object for storing a single rdkafka metric publishing details
17
20
  RdKafkaMetric = Struct.new(:type, :scope, :name, :key_location)
18
21
 
19
22
  # Namespace under which the DD metrics should be published
20
- setting :namespace, default: 'waterdrop', reader: true
23
+ setting :namespace, default: 'waterdrop'
21
24
 
22
25
  # Datadog client that we should use to publish the metrics
23
- setting :client, reader: true
26
+ setting :client
24
27
 
25
28
  # Default tags we want to publish (for example hostname)
26
29
  # Format as followed (example for hostname): `["host:#{Socket.gethostname}"]`
27
- setting :default_tags, default: [], reader: true
30
+ setting :default_tags, default: []
28
31
 
29
32
  # All the rdkafka metrics we want to publish
30
33
  #
31
34
  # By default we publish quite a lot so this can be tuned
32
35
  # Note, that the once with `_d` come from WaterDrop, not rdkafka or Kafka
33
- setting :rd_kafka_metrics, reader: true, default: [
36
+ setting :rd_kafka_metrics, default: [
34
37
  # Client metrics
35
38
  RdKafkaMetric.new(:count, :root, 'calls', 'tx_d'),
36
39
  RdKafkaMetric.new(:histogram, :root, 'queue.size', 'msg_cnt_d'),
@@ -47,8 +50,11 @@ module WaterDrop
47
50
  RdKafkaMetric.new(:gauge, :brokers, 'network.latency.p99', %w[rtt p99])
48
51
  ].freeze
49
52
 
53
+ configure
54
+
50
55
  # @param block [Proc] configuration block
51
56
  def initialize(&block)
57
+ configure
52
58
  setup(&block) if block
53
59
  end
54
60
 
@@ -10,7 +10,15 @@ module WaterDrop
10
10
  # Adds a method that allows us to get the native kafka producer name
11
11
  # @return [String] producer instance name
12
12
  def name
13
- ::Rdkafka::Bindings.rd_kafka_name(@native_kafka)
13
+ unless @_native
14
+ version = ::Gem::Version.new(::Rdkafka::VERSION)
15
+ change = ::Gem::Version.new('0.12.0')
16
+ # 0.12.0 changed how the native producer client reference works.
17
+ # This code supports both older and newer versions of rdkafka
18
+ @_native = version >= change ? @client.native : @native_kafka
19
+ end
20
+
21
+ ::Rdkafka::Bindings.rd_kafka_name(@_native)
14
22
  end
15
23
  end
16
24
  end
@@ -3,5 +3,5 @@
3
3
  # WaterDrop library
4
4
  module WaterDrop
5
5
  # Current WaterDrop version
6
- VERSION = '2.3.0'
6
+ VERSION = '2.3.3'
7
7
  end
data/lib/waterdrop.rb CHANGED
@@ -3,10 +3,11 @@
3
3
  # External components
4
4
  # delegate should be removed because we don't need it, we just add it because of ruby-kafka
5
5
  %w[
6
+ forwardable
6
7
  concurrent/array
7
- dry-configurable
8
+ concurrent/hash
9
+ yaml
8
10
  dry/monitor/notifications
9
- dry-validation
10
11
  rdkafka
11
12
  json
12
13
  zeitwerk
data/waterdrop.gemspec CHANGED
@@ -17,9 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.license = 'MIT'
18
18
 
19
19
  spec.add_dependency 'concurrent-ruby', '>= 1.1'
20
- spec.add_dependency 'dry-configurable', '~> 0.13'
21
20
  spec.add_dependency 'dry-monitor', '~> 0.5'
22
- spec.add_dependency 'dry-validation', '~> 1.7'
23
21
  spec.add_dependency 'rdkafka', '>= 0.10'
24
22
  spec.add_dependency 'zeitwerk', '~> 2.3'
25
23
 
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: waterdrop
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.0
4
+ version: 2.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maciej Mensfeld
@@ -34,7 +34,7 @@ cert_chain:
34
34
  R2P11bWoCtr70BsccVrN8jEhzwXngMyI2gVt750Y+dbTu1KgRqZKp/ECe7ZzPzXj
35
35
  pIy9vHxTANKYVyI4qj8OrFdEM5BQNu8oQpL0iQ==
36
36
  -----END CERTIFICATE-----
37
- date: 2022-04-03 00:00:00.000000000 Z
37
+ date: 2022-07-18 00:00:00.000000000 Z
38
38
  dependencies:
39
39
  - !ruby/object:Gem::Dependency
40
40
  name: concurrent-ruby
@@ -50,20 +50,6 @@ dependencies:
50
50
  - - ">="
51
51
  - !ruby/object:Gem::Version
52
52
  version: '1.1'
53
- - !ruby/object:Gem::Dependency
54
- name: dry-configurable
55
- requirement: !ruby/object:Gem::Requirement
56
- requirements:
57
- - - "~>"
58
- - !ruby/object:Gem::Version
59
- version: '0.13'
60
- type: :runtime
61
- prerelease: false
62
- version_requirements: !ruby/object:Gem::Requirement
63
- requirements:
64
- - - "~>"
65
- - !ruby/object:Gem::Version
66
- version: '0.13'
67
53
  - !ruby/object:Gem::Dependency
68
54
  name: dry-monitor
69
55
  requirement: !ruby/object:Gem::Requirement
@@ -78,20 +64,6 @@ dependencies:
78
64
  - - "~>"
79
65
  - !ruby/object:Gem::Version
80
66
  version: '0.5'
81
- - !ruby/object:Gem::Dependency
82
- name: dry-validation
83
- requirement: !ruby/object:Gem::Requirement
84
- requirements:
85
- - - "~>"
86
- - !ruby/object:Gem::Version
87
- version: '1.7'
88
- type: :runtime
89
- prerelease: false
90
- version_requirements: !ruby/object:Gem::Requirement
91
- requirements:
92
- - - "~>"
93
- - !ruby/object:Gem::Version
94
- version: '1.7'
95
67
  - !ruby/object:Gem::Dependency
96
68
  name: rdkafka
97
69
  requirement: !ruby/object:Gem::Requirement
@@ -144,8 +116,14 @@ files:
144
116
  - docker-compose.yml
145
117
  - lib/waterdrop.rb
146
118
  - lib/waterdrop/config.rb
119
+ - lib/waterdrop/configurable.rb
120
+ - lib/waterdrop/configurable/leaf.rb
121
+ - lib/waterdrop/configurable/node.rb
122
+ - lib/waterdrop/contractable.rb
123
+ - lib/waterdrop/contractable/contract.rb
124
+ - lib/waterdrop/contractable/result.rb
125
+ - lib/waterdrop/contractable/rule.rb
147
126
  - lib/waterdrop/contracts.rb
148
- - lib/waterdrop/contracts/base.rb
149
127
  - lib/waterdrop/contracts/config.rb
150
128
  - lib/waterdrop/contracts/message.rb
151
129
  - lib/waterdrop/errors.rb
@@ -192,7 +170,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
192
170
  - !ruby/object:Gem::Version
193
171
  version: '0'
194
172
  requirements: []
195
- rubygems_version: 3.3.4
173
+ rubygems_version: 3.3.7
196
174
  signing_key:
197
175
  specification_version: 4
198
176
  summary: Kafka messaging made easy!
metadata.gz.sig CHANGED
Binary file
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module WaterDrop
4
- module Contracts
5
- # Base for all the contracts in WaterDrop
6
- class Base < Dry::Validation::Contract
7
- config.messages.load_paths << File.join(WaterDrop.gem_root, 'config', 'errors.yml')
8
-
9
- # @param data [Hash] data for validation
10
- # @param error_class [Class] error class that should be used when validation fails
11
- # @return [Boolean] true
12
- # @raise [StandardError] any error provided in the error_class that inherits from the
13
- # standard error
14
- def validate!(data, error_class)
15
- result = call(data)
16
-
17
- return true if result.success?
18
-
19
- raise error_class, result.errors.to_h
20
- end
21
- end
22
- end
23
- end