kafka_rest_client 0.2.2 → 0.3.0

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
  SHA1:
3
- metadata.gz: 9f49da809a9a1da43ec33c8d475ff31055ca46cd
4
- data.tar.gz: f75cc6a67c1806727403dc59f7bb755566fa680c
3
+ metadata.gz: a2547fd6407a534cd9dcc57623b629f2a4dc3f67
4
+ data.tar.gz: 46678ca71d81e2ab8d4618c54ef6ae89ac1a912c
5
5
  SHA512:
6
- metadata.gz: 9cc1954c043763172bc80baed3da9457e504c8e9e231a59d8711d6938e83c792b26be9f4fdc1d222522a0c9a781737bff605120b97420faec9efe05a32270729
7
- data.tar.gz: 186bed143363206e04128674fa49b91d4c0ca621261599a3fa9f82816de7293fa00f4c1a726b9a6b8f5345ff1fad0261bbd60e95e1d64babc9e8b4f762b780f1
6
+ metadata.gz: ebf98c38adac856ae66952a02081451aebcac5f3406ce157c7656719dd4582ebce5ef39eecc65623b90349c5b92a83f25f92054d14ae32c7c0187c1eb1b87649
7
+ data.tar.gz: 69768766374ee6d1d765251888e5c9dae0173470fdccaea8015681768b33743ffd08f3814459ef75d1dc30a9fd5ec22402113a569421b1588470fbb0e30cc309
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A Ruby client to interact with [Kafka REST Proxy](http://docs.confluent.io/1.0.1/kafka-rest/docs/index.html)
4
4
 
5
- **Current Version:** 0.2.1
5
+ **Current Version:** 0.3.0
6
6
 
7
7
  **Supported Ruby versions:** 2.0, 2.1, 2.2
8
8
 
@@ -11,7 +11,7 @@ A Ruby client to interact with [Kafka REST Proxy](http://docs.confluent.io/1.0.1
11
11
  Add this line to your application's Gemfile:
12
12
 
13
13
  ```ruby
14
- gem 'kafka_rest_client', git: 'git@github.com:FundingCircle/kafka_rest_client.git', tag: 'v0.2.1'
14
+ gem 'kafka_rest_client', git: 'git@github.com:FundingCircle/kafka_rest_client.git', tag: 'v0.3.0'
15
15
  ```
16
16
 
17
17
  ## Usage
@@ -39,6 +39,9 @@ producer = KafkaRestClient::AvroProducer.new
39
39
  # with the "#{topic}-value" name
40
40
  producer.produce('ice-cream-melted', { temperature: 35, unit: 'celsius' })
41
41
 
42
+ # This would post a request to the REST Proxy e.g. :
43
+ # {"id": 1, "temperature": 32, "unit": "celsius"}
44
+
42
45
  # Produce multiple events
43
46
  # The schema_id will be fetched from the schema registry by looking up a schema
44
47
  # with the "#{topic}-value" name
@@ -70,6 +73,26 @@ end
70
73
 
71
74
  producer.produce('ice-cream-melted', [IceCreamMeltedEvent.new(35, 'celsius')])
72
75
 
76
+ # All of the above examples expect events to have union types explicitly defined.
77
+ # For example, if you have a nullable string field, the type annotation needs to be:
78
+ # {"field_name": { "string": "field_value"}}
79
+ # You can provide the following option to automatically annotate nullable fields.
80
+ # ⚠️ This does not currently support nested documents
81
+ producer.produce('ice-cream-melted',
82
+ { temperature: 35, unit: 'celsius' },
83
+ annotate_optional_fields: true,
84
+ value_schema: {"namespace": "com.fundingcircle",
85
+ "type": "record",
86
+ "name": "IceCreamMelted",
87
+ "fields": [{"name": "id", "type": "string"},
88
+ {"name": "temperature","type": "int"},
89
+ {"name": "unit",
90
+ "type": ["null", "string"]
91
+ }]
92
+ })
93
+ # This would post a request to the REST Proxy with the correct type annotation, e.g. :
94
+ # {"id": 1, "temperature": 32, "unit": { "string": "celsius"}}
95
+
73
96
  # Exception handling
74
97
  begin
75
98
  producer.produce('ice-cream-melted', { temperature: 35, unit: 'celsius' })
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = 'kafka_rest_client'
7
- spec.version = '0.2.2'
7
+ spec.version = '0.3.0'
8
8
  spec.authors = ['Funding Circle Engineering']
9
9
  spec.email = ['engineering+kafka_rest_client@fundingcircle.com']
10
10
  spec.licenses = ['BSD-3-Clause']
@@ -1,7 +1,7 @@
1
- require 'kafka_rest_client/errors'
2
1
  require 'kafka_rest_client/configuration'
2
+ require 'kafka_rest_client/errors'
3
+ require 'kafka_rest_client/utils'
3
4
  require 'httparty'
4
- require 'json'
5
5
 
6
6
  module KafkaRestClient
7
7
  class AvroProducer
@@ -24,7 +24,11 @@ module KafkaRestClient
24
24
 
25
25
  def produce(topic, events, options = {})
26
26
  if enabled?
27
- payload = build_event_payload(topic, [events].flatten, options)
27
+ events = [events].flatten.map { |event| event_to_hash event }
28
+ if options[:annotate_optional_fields]
29
+ serialized_events = annotate_events_optional_fields(topic, events, options)
30
+ end
31
+ payload = build_event_payload(topic, (serialized_events || events), options)
28
32
  response = post_event_to_kafka(topic, payload)
29
33
  config.logger.debug("Produced to Kafka topic #{topic}: #{payload.inspect}")
30
34
  response
@@ -33,6 +37,52 @@ module KafkaRestClient
33
37
 
34
38
  private
35
39
 
40
+ def event_to_hash(event)
41
+ if event.respond_to? :as_json
42
+ event.as_json
43
+ else
44
+ Utils.deep_stringify_keys event
45
+ end
46
+ end
47
+
48
+ def annotate_optional_fields(event, fields)
49
+ event.each do |k, v|
50
+ if v.is_a?(Hash)
51
+ fail NestedHashSerializationNotImplemented
52
+ elsif v && fields[k]
53
+ event[k] = { fields[k]['type'] => event[k] }
54
+ end
55
+ end
56
+ end
57
+
58
+ def select_schema(topic, options)
59
+ if options[:value_schema]
60
+ options[:value_schema]
61
+ elsif options[:value_schema_id]
62
+ get_schema_by_id(options[:value_schema_id]).fetch('schema')
63
+ else
64
+ get_latest_schema(topic).fetch('schema')
65
+ end
66
+ end
67
+
68
+ # Transforms an optional schema attribute
69
+ # From {"name"=>"temperature", "type"=>["null", "double"]}
70
+ # To {"temperature"=>{"type"=>"double"}}
71
+ def schema_optional_fields(raw_schema)
72
+ JSON.parse(raw_schema).fetch('fields').inject({}) do |hash, attribute|
73
+ if attribute['type'].include? 'null'
74
+ hash[attribute['name']] = { 'type' => (attribute['type'] - ['null']).first }
75
+ end
76
+ hash
77
+ end
78
+ end
79
+
80
+ def annotate_events_optional_fields(topic, events, options)
81
+ raw_schema = select_schema(topic, options)
82
+ optional_fields = schema_optional_fields(raw_schema)
83
+ events.map { |event| annotate_optional_fields(event, optional_fields) }
84
+ end
85
+
36
86
  def build_event_payload(topic, events, options)
37
87
  payload = { records: events.map { |event| { value: event } } }.merge(
38
88
  options.select { |k, _| [:value_schema_id, :value_schema].include?(k) }
@@ -45,16 +95,13 @@ module KafkaRestClient
45
95
  payload
46
96
  end
47
97
 
48
- def get_latest_schema(topic)
49
- response = HTTParty.get(
50
- "#{config.schema_registry_url}/subjects/#{topic}-value/versions/latest",
51
- timeout: config.timeout
52
- )
98
+ def get_schema(id, url)
99
+ response = HTTParty.get(url, timeout: config.timeout)
53
100
 
54
101
  if response.code == 200
55
102
  JSON.parse(response.body)
56
103
  elsif response.code == 404
57
- fail SchemaNotFoundError, "Schema for #{topic} not found: #{response.body}"
104
+ fail SchemaNotFoundError, "Schema for #{id} not found: #{response.body}"
58
105
  elsif response.code == 500
59
106
  fail InternalServerError, "Schema registry internal error: #{response.body}"
60
107
  elsif response.code == 503
@@ -64,6 +111,16 @@ module KafkaRestClient
64
111
  end
65
112
  end
66
113
 
114
+ def get_schema_by_id(id)
115
+ url = "#{config.schema_registry_url}/schemas/ids/#{id}"
116
+ get_schema(id, url)
117
+ end
118
+
119
+ def get_latest_schema(topic)
120
+ url = "#{config.schema_registry_url}/subjects/#{topic}-value/versions/latest"
121
+ get_schema('latest', url)
122
+ end
123
+
67
124
  def post_event_to_kafka(topic, message)
68
125
  response = HTTParty.post(
69
126
  "#{config.kafka_proxy_url}/topics/#{topic}",
@@ -16,4 +16,6 @@ module KafkaRestClient
16
16
 
17
17
  # Exception raised on a bad gateway proxy resolution failure (503)
18
18
  class BadGateway < Error; end
19
+
20
+ class NestedHashSerializationNotImplemented < Error; end
19
21
  end
@@ -0,0 +1,26 @@
1
+ module KafkaRestClient
2
+ module Utils
3
+ # Courtersy of avdi
4
+ # Source https://gist.github.com/avdi/239567
5
+ def transform_hash(original, options={}, &block)
6
+ original.inject({}){|result, (key,value)|
7
+ value = if (options[:deep] && Hash === value)
8
+ transform_hash(value, options, &block)
9
+ else
10
+ value
11
+ end
12
+ block.call(result,key,value)
13
+ result
14
+ }
15
+ end
16
+
17
+ def deep_stringify_keys(hash)
18
+ transform_hash(hash, :deep => true) {|hash, key, value|
19
+ hash[key.to_s] = value
20
+ }
21
+ end
22
+
23
+ module_function :transform_hash
24
+ module_function :deep_stringify_keys
25
+ end
26
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kafka_rest_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Funding Circle Engineering
@@ -144,6 +144,7 @@ files:
144
144
  - lib/kafka_rest_client/avro_producer.rb
145
145
  - lib/kafka_rest_client/configuration.rb
146
146
  - lib/kafka_rest_client/errors.rb
147
+ - lib/kafka_rest_client/utils.rb
147
148
  homepage: http://github.com/FundingCircle/kakfa_rest_client
148
149
  licenses:
149
150
  - BSD-3-Clause