waterdrop 2.3.0 → 2.3.3

Sign up to get free protection for your applications and to get access to all the features.
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