avro_turf 1.2.0 → 1.4.1

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: 8fc2d29f1112e649cc2cd5ec96f2126a0ea5fa8f46ebee01d908cdceeac8da0d
4
- data.tar.gz: e1989abd9d9b0e5db24f360e96e7bce7ce18e5f488cc794d25a2b3fae6102e66
3
+ metadata.gz: 0d201b8d402f0d88f3ce125040de4e056d0125607b6988236ff076b3fd042569
4
+ data.tar.gz: 0c1cbad2263b35f56dc04ca2def7a9ec520a3c95952acbc27f544a1c96337de8
5
5
  SHA512:
6
- metadata.gz: f562a66ba746d2c0e4bee8b8f3a0e7fcf65900f261eca444986c8ff6c68231e67aa9c4dc659213721d0d09c65fd9273e12e158938c0773baa48cf86598d6bef9
7
- data.tar.gz: 53e3640900a3b64038fe063f5dadab2049417490be1833bb5f17124307abe628af64ea0c68b5bdc2bd4ef2f6e58bbfac287696eaf5ece9a35ddb7c2ab8e5127e
6
+ metadata.gz: 3af9af40e690c6033afb5183d9438ff0785e09a8ac2288873df20f7f50cfd4c439d05dd227d5655e91e490630f4cb677b9ef167be66af072816a07bd2ef1d7a6
7
+ data.tar.gz: 0d440453c9c8ac12aa3cfc07b9348de674e29fbed65809cbf47956468d2c28b62223f213ffa1f1db2726a96cc0707be98e4cb3cc4bfbb0b4229a7982d529a235
@@ -6,13 +6,16 @@ jobs:
6
6
  build:
7
7
 
8
8
  runs-on: ubuntu-latest
9
+ strategy:
10
+ matrix:
11
+ ruby: [2.3, 2.4, 2.5, 2.6, 2.7, 3.0]
9
12
 
10
13
  steps:
11
- - uses: actions/checkout@v1
12
- - name: Set up Ruby 2.6
13
- uses: actions/setup-ruby@v1
14
+ - uses: actions/checkout@v2
15
+ - name: Set up Ruby ${{ matrix.ruby }}
16
+ uses: ruby/setup-ruby@v1
14
17
  with:
15
- ruby-version: 2.6.x
18
+ ruby-version: ${{ matrix.ruby }}
16
19
  - name: Build and test with RSpec
17
20
  run: |
18
21
  gem install bundler
data/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v1.4.1
6
+
7
+ - Purge sub-schemas from cache before re-parsing schema (#151)
8
+
9
+ ## v1.4.0
10
+
11
+ - Add support for Ruby 3 (#146)
12
+ - Add ability to validate message before encoding in `AvroTurf#encode` interface
13
+
14
+ ## v1.3.1
15
+
16
+ - Prevent CachedConfluentSchemaRegistry from caching the 'latest' version (#140)
17
+ - Fix issue with zero length schema cache file (#138)
18
+
19
+ ## v1.3.0
20
+
21
+ - Add support for plain user/password auth to ConfluentSchemaRegistry (#120)
22
+
5
23
  ## v1.2.0
6
24
 
7
25
  - Expose `fetch_schema`, `fetch_schema_by_id` and `register_schema` schema in `Messaging` interface (#117, #119)
data/README.md CHANGED
@@ -18,7 +18,7 @@ The aliases for the original names will be removed in a future release.
18
18
 
19
19
  ## Note about finding nested schemas
20
20
 
21
- As of AvroTurf version 0.12.0, only top-level schemas that have their own .avsc file will be loaded and resolvable by the `AvroTurf::SchemaStore#find` method. This change will likely not affect most users. However, if you use `AvroTurf::SchemaStore#load_schemas!` to pre-cache all your schemas and then rely on `AvroTurf::SchemaStore#find` to access nested schemas that are not defined by their own .avsc files, your code may stop working when you upgrade to v0.12.0.
21
+ As of AvroTurf version 1.0.0, only top-level schemas that have their own .avsc file will be loaded and resolvable by the `AvroTurf::SchemaStore#find` method. This change will likely not affect most users. However, if you use `AvroTurf::SchemaStore#load_schemas!` to pre-cache all your schemas and then rely on `AvroTurf::SchemaStore#find` to access nested schemas that are not defined by their own .avsc files, your code may stop working when you upgrade to v1.0.0.
22
22
 
23
23
  As an example, if you have a `person` schema (defined in `my/schemas/contacts/person.avsc`) that defines a nested `address` schema like this:
24
24
 
@@ -44,7 +44,7 @@ As an example, if you have a `person` schema (defined in `my/schemas/contacts/pe
44
44
  ]
45
45
  }
46
46
  ```
47
- ...this will no longer work in v0.12.0:
47
+ ...this will no longer work in v1.0.0:
48
48
  ```ruby
49
49
  store = AvroTurf::SchemaStore.new(path: 'my/schemas')
50
50
  store.load_schemas!
@@ -88,6 +88,10 @@ avro.decode(encoded_data, schema_name: "person")
88
88
 
89
89
  # Encode some data using the named schema.
90
90
  avro.encode({ "name" => "Jane", "age" => 28 }, schema_name: "person")
91
+
92
+ # Data can be validated before encoding to get a description of problem through
93
+ # Avro::SchemaValidator::ValidationError exception
94
+ avro.encode({ "titl" => "hello, world" }, schema_name: "person", validate: true)
91
95
  ```
92
96
 
93
97
  ### Inter-schema references
data/lib/avro_turf.rb CHANGED
@@ -40,12 +40,15 @@ class AvroTurf
40
40
  #
41
41
  # data - The data that should be encoded.
42
42
  # schema_name - The name of a schema in the `schemas_path`.
43
+ # validate - The boolean for performing complete data validation before
44
+ # encoding it, Avro::SchemaValidator::ValidationError with
45
+ # a descriptive message will be raised in case of invalid message.
43
46
  #
44
47
  # Returns a String containing the encoded data.
45
- def encode(data, schema_name: nil, namespace: @namespace)
48
+ def encode(data, schema_name: nil, namespace: @namespace, validate: false)
46
49
  stream = StringIO.new
47
50
 
48
- encode_to_stream(data, stream: stream, schema_name: schema_name, namespace: namespace)
51
+ encode_to_stream(data, stream: stream, schema_name: schema_name, namespace: namespace, validate: validate)
49
52
 
50
53
  stream.string
51
54
  end
@@ -56,12 +59,19 @@ class AvroTurf
56
59
  # data - The data that should be encoded.
57
60
  # schema_name - The name of a schema in the `schemas_path`.
58
61
  # stream - An IO object that the encoded data should be written to (optional).
62
+ # validate - The boolean for performing complete data validation before
63
+ # encoding it, Avro::SchemaValidator::ValidationError with
64
+ # a descriptive message will be raised in case of invalid message.
59
65
  #
60
66
  # Returns nothing.
61
- def encode_to_stream(data, schema_name: nil, stream: nil, namespace: @namespace)
67
+ def encode_to_stream(data, schema_name: nil, stream: nil, namespace: @namespace, validate: false)
62
68
  schema = @schema_store.find(schema_name, namespace)
63
69
  writer = Avro::IO::DatumWriter.new(schema)
64
70
 
71
+ if validate
72
+ Avro::SchemaValidator.validate!(schema, data, recursive: true, encoded: false, fail_on_extra_fields: true)
73
+ end
74
+
65
75
  dw = Avro::DataFile::Writer.new(stream, writer, schema, @codec)
66
76
  dw << data.as_avro
67
77
  dw.close
@@ -33,6 +33,8 @@ class AvroTurf::CachedConfluentSchemaRegistry
33
33
  end
34
34
 
35
35
  def subject_version(subject, version = 'latest')
36
+ return @upstream.subject_version(subject, version) if version == 'latest'
37
+
36
38
  @cache.lookup_by_version(subject, version) ||
37
39
  @cache.store_by_version(subject, version, @upstream.subject_version(subject, version))
38
40
  end
@@ -7,6 +7,8 @@ class AvroTurf::ConfluentSchemaRegistry
7
7
  url,
8
8
  logger: Logger.new($stdout),
9
9
  proxy: nil,
10
+ user: nil,
11
+ password: nil,
10
12
  client_cert: nil,
11
13
  client_key: nil,
12
14
  client_key_pass: nil,
@@ -17,10 +19,12 @@ class AvroTurf::ConfluentSchemaRegistry
17
19
  headers = {
18
20
  "Content-Type" => CONTENT_TYPE
19
21
  }
20
- headers[:proxy] = proxy if proxy&.present?
22
+ headers[:proxy] = proxy unless proxy.nil?
21
23
  @connection = Excon.new(
22
24
  url,
23
25
  headers: headers,
26
+ user: user,
27
+ password: password,
24
28
  client_cert: client_cert,
25
29
  client_key: client_key,
26
30
  client_key_pass: client_key_pass,
@@ -36,9 +40,7 @@ class AvroTurf::ConfluentSchemaRegistry
36
40
  end
37
41
 
38
42
  def register(subject, schema)
39
- data = post("/subjects/#{subject}/versions", body: {
40
- schema: schema.to_s
41
- }.to_json)
43
+ data = post("/subjects/#{subject}/versions", body: { schema: schema.to_s }.to_json)
42
44
 
43
45
  id = data.fetch("id")
44
46
 
@@ -78,8 +80,7 @@ class AvroTurf::ConfluentSchemaRegistry
78
80
  # http://docs.confluent.io/3.1.2/schema-registry/docs/api.html#compatibility
79
81
  def compatible?(subject, schema, version = 'latest')
80
82
  data = post("/compatibility/subjects/#{subject}/versions/#{version}",
81
- expects: [200, 404],
82
- body: { schema: schema.to_s }.to_json)
83
+ expects: [200, 404], body: { schema: schema.to_s }.to_json)
83
84
  data.fetch('is_compatible', false) unless data.has_key?('error_code')
84
85
  end
85
86
 
@@ -90,7 +91,7 @@ class AvroTurf::ConfluentSchemaRegistry
90
91
 
91
92
  # Update global config
92
93
  def update_global_config(config)
93
- put("/config", { body: config.to_json })
94
+ put("/config", body: config.to_json)
94
95
  end
95
96
 
96
97
  # Get config for subject
@@ -100,7 +101,7 @@ class AvroTurf::ConfluentSchemaRegistry
100
101
 
101
102
  # Update config for subject
102
103
  def update_subject_config(subject, config)
103
- put("/config/#{subject}", { body: config.to_json })
104
+ put("/config/#{subject}", body: config.to_json)
104
105
  end
105
106
 
106
107
  private
@@ -2,15 +2,19 @@
2
2
  # Extends the InMemoryCache to provide a write-thru to disk for persistent cache.
3
3
  class AvroTurf::DiskCache < AvroTurf::InMemoryCache
4
4
 
5
- def initialize(disk_path)
5
+ def initialize(disk_path, logger: Logger.new($stdout))
6
6
  super()
7
7
 
8
+ @logger = logger
9
+
8
10
  # load the write-thru cache on startup, if it exists
9
11
  @schemas_by_id_path = File.join(disk_path, 'schemas_by_id.json')
10
- @schemas_by_id = JSON.parse(File.read(@schemas_by_id_path)) if File.exist?(@schemas_by_id_path)
12
+ hash = read_from_disk_cache(@schemas_by_id_path)
13
+ @schemas_by_id = hash if hash
11
14
 
12
15
  @ids_by_schema_path = File.join(disk_path, 'ids_by_schema.json')
13
- @ids_by_schema = JSON.parse(File.read(@ids_by_schema_path)) if File.exist?(@ids_by_schema_path)
16
+ hash = read_from_disk_cache(@ids_by_schema_path)
17
+ @ids_by_schema = hash if hash
14
18
 
15
19
  @schemas_by_subject_version_path = File.join(disk_path, 'schemas_by_subject_version.json')
16
20
  @schemas_by_subject_version = {}
@@ -31,12 +35,18 @@ class AvroTurf::DiskCache < AvroTurf::InMemoryCache
31
35
  return value
32
36
  end
33
37
 
34
- # override to include write-thru cache after storing result from upstream
38
+ # override to use a json serializable cache key
39
+ def lookup_by_schema(subject, schema)
40
+ key = "#{subject}#{schema}"
41
+ @ids_by_schema[key]
42
+ end
43
+
44
+ # override to use a json serializable cache key and update the file cache
35
45
  def store_by_schema(subject, schema, id)
36
- # must return the value from storing the result (i.e. do not return result from file write)
37
- value = super
46
+ key = "#{subject}#{schema}"
47
+ @ids_by_schema[key] = id
38
48
  File.write(@ids_by_schema_path, JSON.pretty_generate(@ids_by_schema))
39
- return value
49
+ id
40
50
  end
41
51
 
42
52
  # checks instance var (in-memory cache) for schema
@@ -49,7 +59,7 @@ class AvroTurf::DiskCache < AvroTurf::InMemoryCache
49
59
 
50
60
  return schema unless schema.nil?
51
61
 
52
- hash = JSON.parse(File.read(@schemas_by_subject_version_path)) if File.exist?(@schemas_by_subject_version_path)
62
+ hash = read_from_disk_cache(@schemas_by_subject_version_path)
53
63
  if hash
54
64
  @schemas_by_subject_version = hash
55
65
  @schemas_by_subject_version[key]
@@ -63,7 +73,7 @@ class AvroTurf::DiskCache < AvroTurf::InMemoryCache
63
73
  # update instance var (in memory-cache) to match
64
74
  def store_by_version(subject, version, schema)
65
75
  key = "#{subject}#{version}"
66
- hash = JSON.parse(File.read(@schemas_by_subject_version_path)) if File.exist?(@schemas_by_subject_version_path)
76
+ hash = read_from_disk_cache(@schemas_by_subject_version_path)
67
77
  hash = if hash
68
78
  hash[key] = schema
69
79
  hash
@@ -77,6 +87,19 @@ class AvroTurf::DiskCache < AvroTurf::InMemoryCache
77
87
  @schemas_by_subject_version[key]
78
88
  end
79
89
 
90
+ # Parse the file from disk, if it exists and is not zero length
91
+ private def read_from_disk_cache(path)
92
+ if File.exist?(path)
93
+ if File.size(path)!=0
94
+ return JSON.parse(File.read(path))
95
+ else
96
+ # just log a message if skipping zero length file
97
+ @logger.warn "skipping JSON.parse of zero length file at #{path}"
98
+ end
99
+ end
100
+ return nil
101
+ end
102
+
80
103
  private def write_to_disk_cache(path, hash)
81
104
  File.write(path, JSON.pretty_generate(hash))
82
105
  end
@@ -17,12 +17,12 @@ class AvroTurf::InMemoryCache
17
17
  end
18
18
 
19
19
  def lookup_by_schema(subject, schema)
20
- key = subject + schema.to_s
20
+ key = [subject, schema.to_s]
21
21
  @ids_by_schema[key]
22
22
  end
23
23
 
24
24
  def store_by_schema(subject, schema, id)
25
- key = subject + schema.to_s
25
+ key = [subject, schema.to_s]
26
26
  @ids_by_schema[key] = id
27
27
  end
28
28
 
@@ -34,6 +34,8 @@ class AvroTurf
34
34
  # namespace - The String default schema namespace.
35
35
  # logger - The Logger that should be used to log information (optional).
36
36
  # proxy - Forward the request via proxy (optional).
37
+ # user - User for basic auth (optional).
38
+ # password - Password for basic auth (optional).
37
39
  # client_cert - Name of file containing client certificate (optional).
38
40
  # client_key - Name of file containing client private key to go with client_cert (optional).
39
41
  # client_key_pass - Password to go with client_key (optional).
@@ -47,6 +49,8 @@ class AvroTurf
47
49
  namespace: nil,
48
50
  logger: nil,
49
51
  proxy: nil,
52
+ user: nil,
53
+ password: nil,
50
54
  client_cert: nil,
51
55
  client_key: nil,
52
56
  client_key_pass: nil,
@@ -61,6 +65,8 @@ class AvroTurf
61
65
  registry_url,
62
66
  logger: @logger,
63
67
  proxy: proxy,
68
+ user: user,
69
+ password: password,
64
70
  client_cert: client_cert,
65
71
  client_key: client_key,
66
72
  client_key_pass: client_key_pass,
@@ -118,7 +124,7 @@ class AvroTurf
118
124
  writer.write(message, encoder)
119
125
 
120
126
  stream.string
121
- rescue Excon::Error::NotFound
127
+ rescue Excon::Errors::NotFound
122
128
  if schema_id
123
129
  raise SchemaNotFoundError.new("Schema with id: #{schema_id} is not found on registry")
124
130
  else
@@ -188,8 +194,10 @@ class AvroTurf
188
194
 
189
195
  # Fetch the schema from registry with the provided schema_id.
190
196
  def fetch_schema_by_id(schema_id)
191
- schema_json = @registry.fetch(schema_id)
192
- schema = Avro::Schema.parse(schema_json)
197
+ schema = @schemas_by_id.fetch(schema_id) do
198
+ schema_json = @registry.fetch(schema_id)
199
+ Avro::Schema.parse(schema_json)
200
+ end
193
201
  [schema, schema_id]
194
202
  end
195
203
 
@@ -22,7 +22,7 @@ class AvroTurf::SchemaStore
22
22
  # Still need to check is the schema already loaded
23
23
  return @schemas[fullname] if @schemas.key?(fullname)
24
24
 
25
- load_schema!(fullname, namespace)
25
+ load_schema!(fullname)
26
26
  end
27
27
  end
28
28
 
@@ -42,13 +42,12 @@ class AvroTurf::SchemaStore
42
42
  end
43
43
  end
44
44
 
45
- private
45
+ protected
46
46
 
47
47
  # Loads single schema
48
48
  # Such method is not thread-safe, do not call it of from mutex synchronization routine
49
- def load_schema!(fullname, namespace = nil, local_schemas_cache = {})
50
- *namespace, schema_name = fullname.split(".")
51
- schema_path = File.join(@path, *namespace, schema_name + ".avsc")
49
+ def load_schema!(fullname, local_schemas_cache = {})
50
+ schema_path = build_schema_path(fullname)
52
51
  schema_json = JSON.parse(File.read(schema_path))
53
52
 
54
53
  schema = Avro::Schema.real_parse(schema_json, local_schemas_cache)
@@ -78,17 +77,27 @@ class AvroTurf::SchemaStore
78
77
  # Try to first resolve a referenced schema from disk.
79
78
  # If this is successful, the Avro gem will have mutated the
80
79
  # local_schemas_cache, adding all the new schemas it found.
81
- load_schema!($1, nil, local_schemas_cache)
80
+ load_schema!($1, local_schemas_cache)
82
81
 
83
82
  # Attempt to re-parse the original schema now that the dependency
84
83
  # has been resolved and use the now-updated local_schemas_cache to
85
84
  # pick up where we left off.
86
85
  local_schemas_cache.delete(fullname)
87
- load_schema!(fullname, nil, local_schemas_cache)
86
+ # Ensure all sub-schemas are cleaned up to avoid conflicts when re-parsing
87
+ # schema.
88
+ local_schemas_cache.each do |schema_name, schema|
89
+ local_schemas_cache.delete(schema_name) unless File.exist?(build_schema_path(schema_name))
90
+ end
91
+ load_schema!(fullname, local_schemas_cache)
88
92
  else
89
93
  raise
90
94
  end
91
95
  rescue Errno::ENOENT, Errno::ENAMETOOLONG
92
96
  raise AvroTurf::SchemaNotFoundError, "could not find Avro schema at `#{schema_path}'"
93
97
  end
98
+
99
+ def build_schema_path(fullname)
100
+ *namespace, schema_name = fullname.split(".")
101
+ schema_path = File.join(@path, *namespace, schema_name + ".avsc")
102
+ end
94
103
  end
@@ -1,3 +1,3 @@
1
1
  class AvroTurf
2
- VERSION = "1.2.0"
2
+ VERSION = "1.4.1"
3
3
  end
@@ -6,44 +6,130 @@ describe AvroTurf do
6
6
  end
7
7
 
8
8
  describe "#encode" do
9
- before do
10
- define_schema "person.avsc", <<-AVSC
11
- {
12
- "name": "person",
13
- "type": "record",
14
- "fields": [
15
- {
16
- "type": "string",
17
- "name": "full_name"
18
- }
19
- ]
9
+ context "when using plain schema" do
10
+ before do
11
+ define_schema "person.avsc", <<-AVSC
12
+ {
13
+ "name": "person",
14
+ "type": "record",
15
+ "fields": [
16
+ {
17
+ "type": "string",
18
+ "name": "full_name"
19
+ }
20
+ ]
21
+ }
22
+ AVSC
23
+ end
24
+
25
+ it "encodes data with Avro" do
26
+ data = {
27
+ "full_name" => "John Doe"
20
28
  }
21
- AVSC
22
- end
23
29
 
24
- it "encodes data with Avro" do
25
- data = {
26
- "full_name" => "John Doe"
27
- }
30
+ encoded_data = avro.encode(data, schema_name: "person")
28
31
 
29
- encoded_data = avro.encode(data, schema_name: "person")
32
+ expect(avro.decode(encoded_data)).to eq(data)
33
+ end
30
34
 
31
- expect(avro.decode(encoded_data)).to eq(data)
35
+ it "allows specifying a codec that should be used to compress messages" do
36
+ compressed_avro = AvroTurf.new(schemas_path: "spec/schemas/", codec: "deflate")
37
+
38
+ data = {
39
+ "full_name" => "John Doe" * 100
40
+ }
41
+
42
+ uncompressed_data = avro.encode(data, schema_name: "person")
43
+ compressed_data = compressed_avro.encode(data, schema_name: "person")
44
+
45
+ expect(compressed_data.bytesize).to be < uncompressed_data.bytesize
46
+ expect(compressed_avro.decode(compressed_data)).to eq(data)
47
+ end
32
48
  end
33
49
 
34
- it "allows specifying a codec that should be used to compress messages" do
35
- compressed_avro = AvroTurf.new(schemas_path: "spec/schemas/", codec: "deflate")
50
+ context 'when using nested schemas' do
51
+ before do
52
+ define_schema "post.avsc", <<-AVSC
53
+ {
54
+ "name": "post",
55
+ "type": "record",
56
+ "fields": [
57
+ {
58
+ "name": "tag",
59
+ "type": {
60
+ "type": "enum",
61
+ "name": "tag",
62
+ "symbols": ["foo", "bar"]
63
+ }
64
+ },
65
+ {
66
+ "name": "messages",
67
+ "type": {
68
+ "type": "array",
69
+ "items": "message"
70
+ }
71
+ },
72
+ {
73
+ "name": "status",
74
+ "type": "publishing_status"
75
+ }
76
+ ]
77
+ }
78
+ AVSC
79
+
80
+ define_schema "publishing_status.avsc", <<-AVSC
81
+ {
82
+ "name": "publishing_status",
83
+ "type": "enum",
84
+ "symbols": ["draft", "published", "archived"]
85
+ }
86
+ AVSC
36
87
 
37
- data = {
38
- "full_name" => "John Doe" * 100
39
- }
88
+ define_schema "message.avsc", <<-AVSC
89
+ {
90
+ "name": "message",
91
+ "type": "record",
92
+ "fields": [
93
+ {
94
+ "type": "string",
95
+ "name": "content"
96
+ },
97
+ {
98
+ "name": "label",
99
+ "type": {
100
+ "type": "enum",
101
+ "name": "label",
102
+ "symbols": ["foo", "bar"]
103
+ }
104
+ },
105
+ {
106
+ "name": "status",
107
+ "type": "publishing_status"
108
+ }
109
+ ]
110
+ }
111
+ AVSC
112
+ end
113
+
114
+ it "encodes data with Avro" do
115
+ data = {
116
+ "tag" => "foo",
117
+ "messages" => [
118
+ {
119
+ "content" => "hello",
120
+ "label" => "bar",
121
+ "status" => "draft"
122
+ }
123
+ ],
124
+ "status" => "published"
125
+ }
40
126
 
41
- uncompressed_data = avro.encode(data, schema_name: "person")
42
- compressed_data = compressed_avro.encode(data, schema_name: "person")
127
+ encoded_data = avro.encode(data, schema_name: "post")
43
128
 
44
- expect(compressed_data.bytesize).to be < uncompressed_data.bytesize
45
- expect(compressed_avro.decode(compressed_data)).to eq(data)
129
+ expect(avro.decode(encoded_data)).to eq(data)
130
+ end
46
131
  end
132
+
47
133
  end
48
134
 
49
135
  describe "#decode" do
@@ -105,6 +191,67 @@ describe AvroTurf do
105
191
 
106
192
  expect(avro.decode(stream.string)).to eq "hello"
107
193
  end
194
+
195
+ context "validating" do
196
+ subject(:encode_to_stream) do
197
+ stream = StringIO.new
198
+ avro.encode_to_stream(message, stream: stream, schema_name: "message", validate: true)
199
+ end
200
+
201
+ context "with a valid message" do
202
+ let(:message) { { "full_name" => "John Doe" } }
203
+
204
+ it "does not raise any error" do
205
+ define_schema "message.avsc", <<-AVSC
206
+ {
207
+ "name": "message",
208
+ "type": "record",
209
+ "fields": [
210
+ { "name": "full_name", "type": "string" }
211
+ ]
212
+ }
213
+ AVSC
214
+
215
+ expect { encode_to_stream }.not_to raise_error
216
+ end
217
+ end
218
+
219
+ context "when message has wrong type" do
220
+ let(:message) { { "full_name" => 123 } }
221
+
222
+ it "raises Avro::SchemaValidator::ValidationError with a message about type mismatch" do
223
+ define_schema "message.avsc", <<-AVSC
224
+ {
225
+ "name": "message",
226
+ "type": "record",
227
+ "fields": [
228
+ { "name": "full_name", "type": "string" }
229
+ ]
230
+ }
231
+ AVSC
232
+
233
+ expect { encode_to_stream }.to raise_error(Avro::SchemaValidator::ValidationError, /\.full_name expected type string, got int/)
234
+ end
235
+ end
236
+
237
+ context "when message contains extra fields (typo in key)" do
238
+ let(:message) { { "fulll_name" => "John Doe" } }
239
+
240
+ it "raises Avro::SchemaValidator::ValidationError with a message about extra field" do
241
+ define_schema "message.avsc", <<-AVSC
242
+ {
243
+ "name": "message",
244
+ "type": "record",
245
+ "fields": [
246
+ { "name": "full_name", "type": "string" }
247
+ ]
248
+ }
249
+ AVSC
250
+
251
+ expect { encode_to_stream }.to raise_error(Avro::SchemaValidator::ValidationError, /extra field 'fulll_name'/)
252
+ end
253
+ end
254
+ end
108
255
  end
109
256
 
110
257
  describe "#decode_stream" do
@@ -3,6 +3,8 @@ require 'avro_turf/confluent_schema_registry'
3
3
  require 'avro_turf/test/fake_confluent_schema_registry_server'
4
4
 
5
5
  describe AvroTurf::ConfluentSchemaRegistry do
6
+ let(:user) { "abc" }
7
+ let(:password) { "xxyyzz" }
6
8
  let(:client_cert) { "test client cert" }
7
9
  let(:client_key) { "test client key" }
8
10
  let(:client_key_pass) { "test client key password" }
@@ -18,4 +20,14 @@ describe AvroTurf::ConfluentSchemaRegistry do
18
20
  )
19
21
  }
20
22
  end
23
+
24
+ it_behaves_like "a confluent schema registry client" do
25
+ let(:registry) {
26
+ described_class.new(
27
+ registry_url,
28
+ user: user,
29
+ password: password,
30
+ )
31
+ }
32
+ end
21
33
  end
@@ -4,7 +4,8 @@ require 'avro_turf/test/fake_confluent_schema_registry_server'
4
4
 
5
5
  describe AvroTurf::CachedConfluentSchemaRegistry do
6
6
  let(:upstream) { instance_double(AvroTurf::ConfluentSchemaRegistry) }
7
- let(:cache) { AvroTurf::DiskCache.new("spec/cache")}
7
+ let(:logger_io) { StringIO.new }
8
+ let(:cache) { AvroTurf::DiskCache.new("spec/cache", logger: Logger.new(logger_io))}
8
9
  let(:registry) { described_class.new(upstream, cache: cache) }
9
10
  let(:id) { rand(999) }
10
11
  let(:schema) do
@@ -80,6 +81,40 @@ describe AvroTurf::CachedConfluentSchemaRegistry do
80
81
  end
81
82
  end
82
83
 
84
+ describe "#fetch (zero length cache file)" do
85
+ let(:cache_after) do
86
+ {
87
+ "#{id}" => "#{schema}"
88
+ }
89
+ end
90
+
91
+ before do
92
+ # setup the disk cache with a zero length file
93
+ File.write(File.join("spec/cache", "schemas_by_id.json"), '')
94
+ end
95
+
96
+ it "skips zero length disk cache" do
97
+ # multiple calls return same result, with only one upstream call
98
+ allow(upstream).to receive(:fetch).with(id).and_return(schema)
99
+ expect(registry.fetch(id)).to eq(schema)
100
+ expect(registry.fetch(id)).to eq(schema)
101
+ expect(upstream).to have_received(:fetch).exactly(1).times
102
+ expect(load_cache("schemas_by_id.json")).to eq cache_after
103
+ expect(logger_io.string).to include("zero length file at spec/cache/schemas_by_id.json")
104
+ end
105
+ end
106
+
107
+ describe "#fetch (corrupt cache file)" do
108
+ before do
109
+ # setup the disk cache with a corrupt file (i.e. not json)
110
+ File.write(File.join("spec/cache", "schemas_by_id.json"), 'NOTJSON')
111
+ end
112
+
113
+ it "raises error on corrupt cache file" do
114
+ expect{registry.fetch(id)}.to raise_error(JSON::ParserError, /unexpected token/)
115
+ end
116
+ end
117
+
83
118
  describe "#register" do
84
119
  let(:subject_name) { "a_subject" }
85
120
  let(:cache_before) do
@@ -120,6 +155,41 @@ describe AvroTurf::CachedConfluentSchemaRegistry do
120
155
  end
121
156
  end
122
157
 
158
+ describe "#register (zero length cache file)" do
159
+ let(:subject_name) { "a_subject" }
160
+ let(:cache_after) do
161
+ {
162
+ "#{subject_name}#{schema}" => id
163
+ }
164
+ end
165
+
166
+ before do
167
+ # setup the disk cache with a zero length file
168
+ File.write(File.join("spec/cache", "ids_by_schema.json"), '')
169
+ end
170
+
171
+ it "skips zero length disk cache" do
172
+ # multiple calls return same result, with only one upstream call
173
+ allow(upstream).to receive(:register).with(subject_name, schema).and_return(id)
174
+ expect(registry.register(subject_name, schema)).to eq(id)
175
+ expect(registry.register(subject_name, schema)).to eq(id)
176
+ expect(upstream).to have_received(:register).exactly(1).times
177
+ expect(load_cache("ids_by_schema.json")).to eq cache_after
178
+ expect(logger_io.string).to include("zero length file at spec/cache/ids_by_schema.json")
179
+ end
180
+ end
181
+
182
+ describe "#register (corrupt cache file)" do
183
+ before do
184
+ # setup the disk cache with a corrupt file (i.e. not json)
185
+ File.write(File.join("spec/cache", "ids_by_schema.json"), 'NOTJSON')
186
+ end
187
+
188
+ it "raises error on corrupt cache file" do
189
+ expect{registry.register(subject_name, schema)}.to raise_error(JSON::ParserError, /unexpected token/)
190
+ end
191
+ end
192
+
123
193
  describe "#subject_version" do
124
194
  it "writes thru to disk cache" do
125
195
  # multiple calls return same result, with zero upstream calls
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: avro_turf
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Schierbeck
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-10 00:00:00.000000000 Z
11
+ date: 2021-07-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: avro
@@ -163,7 +163,6 @@ executables: []
163
163
  extensions: []
164
164
  extra_rdoc_files: []
165
165
  files:
166
- - ".circleci/config.yml"
167
166
  - ".github/workflows/ruby.yml"
168
167
  - ".github/workflows/stale.yml"
169
168
  - ".gitignore"
@@ -250,8 +249,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
250
249
  - !ruby/object:Gem::Version
251
250
  version: '0'
252
251
  requirements: []
253
- rubyforge_project:
254
- rubygems_version: 2.7.6
252
+ rubygems_version: 3.1.2
255
253
  signing_key:
256
254
  specification_version: 4
257
255
  summary: A library that makes it easier to use the Avro serialization format from
data/.circleci/config.yml DELETED
@@ -1,36 +0,0 @@
1
- version: 2
2
- jobs:
3
- build:
4
- environment:
5
- CIRCLE_ARTIFACTS: /tmp/circleci-artifacts
6
- CIRCLE_TEST_REPORTS: /tmp/circleci-test-results
7
- docker:
8
- - image: circleci/ruby:2.6.2
9
- steps:
10
- - checkout
11
- - run: mkdir -p $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS
12
- - restore_cache:
13
- keys:
14
- # This branch if available
15
- - v1-dep-{{ .Branch }}-
16
- # Default branch if not
17
- - v1-dep-master-
18
- # Any branch if there are none on the default branch - this should be unnecessary if you have your default branch configured correctly
19
- - v1-dep-
20
- - run: gem install bundler --no-document
21
- - run: 'bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs=4 --retry=3'
22
- # Save dependency cache
23
- - save_cache:
24
- key: v1-dep-{{ .Branch }}-{{ epoch }}
25
- paths:
26
- - vendor/bundle
27
- - ~/.bundle
28
- - run: mkdir -p $CIRCLE_TEST_REPORTS/rspec
29
- - run:
30
- command: bundle exec rspec --color --require spec_helper --format progress
31
- - store_test_results:
32
- path: /tmp/circleci-test-results
33
- - store_artifacts:
34
- path: /tmp/circleci-artifacts
35
- - store_artifacts:
36
- path: /tmp/circleci-test-results