schema_registry_client 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/lint.yml +18 -0
  3. data/.github/workflows/release.yml +31 -0
  4. data/.github/workflows/test.yml +22 -0
  5. data/.gitignore +1 -0
  6. data/.rspec +2 -0
  7. data/.rubocop.yml +39 -0
  8. data/CHANGELOG.md +12 -0
  9. data/Gemfile +5 -0
  10. data/Gemfile.lock +150 -0
  11. data/LICENSE +20 -0
  12. data/README.md +48 -0
  13. data/Rakefile +3 -0
  14. data/lib/schema_registry_client/avro_schema_store.rb +127 -0
  15. data/lib/schema_registry_client/cached_confluent_schema_registry.rb +57 -0
  16. data/lib/schema_registry_client/confluent_schema_registry.rb +118 -0
  17. data/lib/schema_registry_client/output/json_schema.rb +78 -0
  18. data/lib/schema_registry_client/output/proto_text.rb +320 -0
  19. data/lib/schema_registry_client/schema/avro.rb +61 -0
  20. data/lib/schema_registry_client/schema/base.rb +44 -0
  21. data/lib/schema_registry_client/schema/proto_json_schema.rb +30 -0
  22. data/lib/schema_registry_client/schema/protobuf.rb +131 -0
  23. data/lib/schema_registry_client/version.rb +5 -0
  24. data/lib/schema_registry_client/wire.rb +30 -0
  25. data/lib/schema_registry_client.rb +156 -0
  26. data/schema_registry_client.gemspec +33 -0
  27. data/spec/decoding_spec.rb +183 -0
  28. data/spec/encoding_spec.rb +207 -0
  29. data/spec/gen/everything/everything_pb.rb +26 -0
  30. data/spec/gen/referenced/referer_pb.rb +24 -0
  31. data/spec/gen/simple/simple_pb.rb +18 -0
  32. data/spec/json_schema_spec.rb +12 -0
  33. data/spec/proto_text_spec.rb +10 -0
  34. data/spec/schemas/everything/everything.json +328 -0
  35. data/spec/schemas/everything/everything.proto +105 -0
  36. data/spec/schemas/referenced/referenced.json +16 -0
  37. data/spec/schemas/referenced/referer.proto +28 -0
  38. data/spec/schemas/referenced/v1/MessageBA.avsc +21 -0
  39. data/spec/schemas/simple/simple.json +12 -0
  40. data/spec/schemas/simple/simple.proto +12 -0
  41. data/spec/schemas/simple/v1/SimpleMessage.avsc +11 -0
  42. data/spec/spec_helper.rb +16 -0
  43. metadata +46 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 19d8a1a9d31add74657d9dcf9102bf5b05cca485f15fbaf50a3d4490ce517a2a
4
- data.tar.gz: 413516fa57982d58ebacd97bfcc087842aba1192e5f7ef622a4082e4c9b09b60
3
+ metadata.gz: 5bd3f6c9d59bc29bfbcc32577761bd90b17282dc63b7423a1d74978235b316c8
4
+ data.tar.gz: fde47855e0974f734724d0c9fbe36b80644be5ed2f42215cbfa389e4951affdb
5
5
  SHA512:
6
- metadata.gz: 5e345a78b8ea4495f1d8a09edc61aed48e0b5c39c9f1e6ca5df69027982916ea4fa5be192e6e99527ee318203ded60ed0a76398843f135541858a978443530b1
7
- data.tar.gz: '086b30ff39befcc21e1e0842809187c81e6adc704b069a38d89213ca1f8865c26c57f6a36e36c886cfa5d59f4f35d02c9aaac4090469524741d9679b17b24653'
6
+ metadata.gz: dd97621d317f2f951c960b2f928832004c8d38b8c4f0ebdf291bbdb68ed7d873f83f4a789426479ead787f77f1dc00c2a5c1898407373a1fdc27b448c6ed1f97
7
+ data.tar.gz: fe7f21af487e6e8294977e5b50e77bd2a77648df7eb1b9bd4cb56a709fe09e0401ecba9e44ba35854b6383d35fcb2baf3530cf485963f7887a4e1fa8c0fc6dc4
@@ -0,0 +1,18 @@
1
+ name: Lint
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ build:
7
+
8
+ runs-on: ubuntu-latest
9
+
10
+ steps:
11
+ - uses: actions/checkout@v3
12
+ - name: Set up Ruby ${{ matrix.ruby }}
13
+ uses: ruby/setup-ruby@v1
14
+ with:
15
+ ruby-version: 3.4
16
+ bundler-cache: true
17
+ - name: Run standardrb
18
+ run: bundle exec standardrb
@@ -0,0 +1,31 @@
1
+ name: Release Gem
2
+ on:
3
+ push:
4
+ branches:
5
+ - main
6
+ tags:
7
+ - 'v*.*.*' # Matches semantic versioning tags like v1.0.0
8
+ workflow_dispatch: # Allows manual triggering of the workflow
9
+
10
+ jobs:
11
+ push:
12
+ name: Push gem to RubyGems.org
13
+ runs-on: ubuntu-latest
14
+
15
+ permissions:
16
+ id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
17
+ contents: write # IMPORTANT: this permission is required for `rake release` to push the release tag
18
+
19
+ steps:
20
+ # Set up
21
+ - uses: actions/checkout@v4
22
+ with:
23
+ persist-credentials: false
24
+ - name: Set up Ruby
25
+ uses: ruby/setup-ruby@v1
26
+ with:
27
+ bundler-cache: true
28
+ ruby-version: 3.4
29
+
30
+ # Release
31
+ - uses: rubygems/release-gem@v1
@@ -0,0 +1,22 @@
1
+ name: Test
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ build:
7
+
8
+ runs-on: ubuntu-latest
9
+ strategy:
10
+ fail-fast: false
11
+ matrix:
12
+ ruby: [3.1, 3.2, 3.3, 3.4]
13
+
14
+ steps:
15
+ - uses: actions/checkout@v3
16
+ - name: Set up Ruby ${{ matrix.ruby }}
17
+ uses: ruby/setup-ruby@v1
18
+ with:
19
+ ruby-version: ${{ matrix.ruby }}
20
+ bundler-cache: true
21
+ - name: Build and test with RSpec
22
+ run: bundle exec rspec
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ coverage
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,39 @@
1
+ AllCops:
2
+ Exclude:
3
+ - app/gen/**/*
4
+ - spec/gen/**/*
5
+
6
+ Metrics/MethodLength:
7
+ Severity: refactor
8
+ Max: 60
9
+
10
+ Metrics/ModuleLength:
11
+ Severity: refactor
12
+ Max: 800
13
+
14
+ Metrics/CyclomaticComplexity:
15
+ Severity: refactor
16
+ Max: 25
17
+
18
+ Metrics/ClassLength:
19
+ Severity: refactor
20
+ Max: 800
21
+
22
+
23
+ Metrics/BlockLength:
24
+ Enabled: false
25
+
26
+ Metrics/AbcSize:
27
+ Severity: refactor
28
+ Max: 60
29
+
30
+ Metrics/PerceivedComplexity:
31
+ Severity: refactor
32
+ Max: 15
33
+
34
+ Style/Documentation:
35
+ Enabled: false
36
+
37
+ Lint/UnusedMethodArgument:
38
+ AllowUnusedKeywordArguments: true
39
+
data/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## UNRELEASED
9
+
10
+ # 0.0.6 - 2026-01-02
11
+
12
+ * Initial release.
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,150 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ schema_registry_client (0.0.6)
5
+ avro
6
+ excon
7
+ google-protobuf
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ addressable (2.8.8)
13
+ public_suffix (>= 2.0.2, < 8.0)
14
+ ast (2.4.3)
15
+ avro (1.12.1)
16
+ multi_json (~> 1.0)
17
+ bigdecimal (4.0.1)
18
+ crack (1.0.1)
19
+ bigdecimal
20
+ rexml
21
+ diff-lcs (1.6.2)
22
+ docile (1.4.1)
23
+ excon (1.3.2)
24
+ logger
25
+ google-protobuf (4.33.2)
26
+ bigdecimal
27
+ rake (>= 13)
28
+ google-protobuf (4.33.2-aarch64-linux-gnu)
29
+ bigdecimal
30
+ rake (>= 13)
31
+ google-protobuf (4.33.2-aarch64-linux-musl)
32
+ bigdecimal
33
+ rake (>= 13)
34
+ google-protobuf (4.33.2-arm64-darwin)
35
+ bigdecimal
36
+ rake (>= 13)
37
+ google-protobuf (4.33.2-x86-linux-gnu)
38
+ bigdecimal
39
+ rake (>= 13)
40
+ google-protobuf (4.33.2-x86-linux-musl)
41
+ bigdecimal
42
+ rake (>= 13)
43
+ google-protobuf (4.33.2-x86_64-darwin)
44
+ bigdecimal
45
+ rake (>= 13)
46
+ google-protobuf (4.33.2-x86_64-linux-gnu)
47
+ bigdecimal
48
+ rake (>= 13)
49
+ google-protobuf (4.33.2-x86_64-linux-musl)
50
+ bigdecimal
51
+ rake (>= 13)
52
+ hashdiff (1.2.1)
53
+ json (2.18.0)
54
+ language_server-protocol (3.17.0.5)
55
+ lint_roller (1.1.0)
56
+ logger (1.7.0)
57
+ multi_json (1.19.1)
58
+ parallel (1.27.0)
59
+ parser (3.3.10.0)
60
+ ast (~> 2.4.1)
61
+ racc
62
+ prism (1.7.0)
63
+ public_suffix (7.0.0)
64
+ racc (1.8.1)
65
+ rainbow (3.1.1)
66
+ rake (13.3.1)
67
+ regexp_parser (2.11.3)
68
+ rexml (3.4.4)
69
+ rspec (3.13.2)
70
+ rspec-core (~> 3.13.0)
71
+ rspec-expectations (~> 3.13.0)
72
+ rspec-mocks (~> 3.13.0)
73
+ rspec-core (3.13.6)
74
+ rspec-support (~> 3.13.0)
75
+ rspec-expectations (3.13.5)
76
+ diff-lcs (>= 1.2.0, < 2.0)
77
+ rspec-support (~> 3.13.0)
78
+ rspec-mocks (3.13.7)
79
+ diff-lcs (>= 1.2.0, < 2.0)
80
+ rspec-support (~> 3.13.0)
81
+ rspec-support (3.13.6)
82
+ rubocop (1.81.7)
83
+ json (~> 2.3)
84
+ language_server-protocol (~> 3.17.0.2)
85
+ lint_roller (~> 1.1.0)
86
+ parallel (~> 1.10)
87
+ parser (>= 3.3.0.2)
88
+ rainbow (>= 2.2.2, < 4.0)
89
+ regexp_parser (>= 2.9.3, < 3.0)
90
+ rubocop-ast (>= 1.47.1, < 2.0)
91
+ ruby-progressbar (~> 1.7)
92
+ unicode-display_width (>= 2.4.0, < 4.0)
93
+ rubocop-ast (1.49.0)
94
+ parser (>= 3.3.7.2)
95
+ prism (~> 1.7)
96
+ rubocop-performance (1.26.1)
97
+ lint_roller (~> 1.1)
98
+ rubocop (>= 1.75.0, < 2.0)
99
+ rubocop-ast (>= 1.47.1, < 2.0)
100
+ ruby-progressbar (1.13.0)
101
+ simplecov (0.22.0)
102
+ docile (~> 1.1)
103
+ simplecov-html (~> 0.11)
104
+ simplecov_json_formatter (~> 0.1)
105
+ simplecov-html (0.13.2)
106
+ simplecov_json_formatter (0.1.4)
107
+ standard (1.52.0)
108
+ language_server-protocol (~> 3.17.0.2)
109
+ lint_roller (~> 1.0)
110
+ rubocop (~> 1.81.7)
111
+ standard-custom (~> 1.0.0)
112
+ standard-performance (~> 1.8)
113
+ standard-custom (1.0.2)
114
+ lint_roller (~> 1.0)
115
+ rubocop (~> 1.50)
116
+ standard-performance (1.9.0)
117
+ lint_roller (~> 1.1)
118
+ rubocop-performance (~> 1.26.0)
119
+ standardrb (1.0.1)
120
+ standard
121
+ unicode-display_width (3.2.0)
122
+ unicode-emoji (~> 4.1)
123
+ unicode-emoji (4.2.0)
124
+ webmock (3.26.1)
125
+ addressable (>= 2.8.0)
126
+ crack (>= 0.3.2)
127
+ hashdiff (>= 0.4.0, < 2.0.0)
128
+
129
+ PLATFORMS
130
+ aarch64-linux-gnu
131
+ aarch64-linux-musl
132
+ arm64-darwin
133
+ ruby
134
+ x86-linux-gnu
135
+ x86-linux-musl
136
+ x86_64-darwin
137
+ x86_64-linux-gnu
138
+ x86_64-linux-musl
139
+
140
+ DEPENDENCIES
141
+ bundler (~> 2.0)
142
+ rake (~> 13.0)
143
+ rspec (~> 3.2)
144
+ schema_registry_client!
145
+ simplecov
146
+ standardrb
147
+ webmock
148
+
149
+ BUNDLED WITH
150
+ 2.6.9
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # schema_registry_client
2
+
3
+ `schema_registry_client` is a library to interact with the Confluent Schema Registry using Google Protobuf. It is inspired by and based off of [avro_turf](https://github.com/dasch/avro_turf).
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'schema_registry_client'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install schema_registry_client
20
+
21
+ ## Usage
22
+
23
+ SchemaRegistry interacts with the Confluent Schema Registry, and caches all results. When you first encode a message, it will register the message and all dependencies with the Schema Registry. When decoding, it will look up the schema in the Schema Registry and use the associated local generated code to decode the message.
24
+
25
+ Example usage:
26
+
27
+ ```ruby
28
+ require 'schema_registry_client'
29
+
30
+ schema_registry_client = SchemaRegistry.new(registry_url: 'http://localhost:8081', schema_paths: ['path/to/protos'])
31
+ message = MyProto::MyMessage.new(field1: 'value1', field2: 42)
32
+ encoded = schema_registry_client.encode(message, subject: 'my-subject')
33
+
34
+ # Decoding
35
+
36
+ decoded_proto_message = schema_registry_client.decode(encoded_string)
37
+ ```
38
+
39
+ ## Notes about usage
40
+
41
+ * When decoding, this library does *not* attempt to fully parse the Proto definition stored on the schema registry and generate dynamic classes. Instead, it simply parses out the package and message and assumes that the reader has the message available in the descriptor pool. Any compatibility issues should be detected through normal means, i.e. just by instantiating the message and seeing if any errors are raised.
42
+
43
+ ### Regenerating test protos
44
+ Run the following to regenerate:
45
+
46
+ ```sh
47
+ protoc -I spec/schemas --ruby_out=spec/gen --ruby_opt=paths=source_relative spec/schemas/**/*.proto
48
+ ```
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'avro'
4
+
5
+ class SchemaRegistry
6
+ class AvroSchemaStore
7
+ def initialize(path: nil)
8
+ @path = path or raise 'Please specify a schema path'
9
+ @schemas = {}
10
+ @schema_text = {}
11
+ @mutex = Mutex.new
12
+ end
13
+
14
+ attr_accessor :schemas
15
+
16
+ def find_text(name)
17
+ @schema_text[name]
18
+ end
19
+
20
+ # Resolves and returns a schema.
21
+ #
22
+ # schema_name - The String name of the schema to resolve.
23
+ #
24
+ # Returns an Avro::Schema.
25
+ def find(name)
26
+ # Optimistic non-blocking read from @schemas
27
+ # No sense to lock the resource when all the schemas already loaded
28
+ return @schemas[name] if @schemas.key?(name)
29
+
30
+ # Pessimistic blocking write to @schemas
31
+ @mutex.synchronize do
32
+ # Still need to check is the schema already loaded
33
+ return @schemas[name] if @schemas.key?(name)
34
+
35
+ load_schema!(name, @schemas.dup)
36
+ end
37
+ end
38
+
39
+ # Loads all schema definition files in the `schemas_dir`.
40
+ def load_schemas!
41
+ pattern = [@path, '**', '*.avsc'].join('/')
42
+
43
+ Dir.glob(pattern) do |schema_path|
44
+ # Remove the path prefix.
45
+ schema_path.sub!(%r{^/?#{@path}/}, '')
46
+
47
+ # Replace `/` with `.` and chop off the file extension.
48
+ schema_name = File.basename(schema_path.tr('/', '.'), '.avsc')
49
+
50
+ # Load and cache the schema.
51
+ find(schema_name)
52
+ end
53
+ end
54
+
55
+ # @param schema_hash [Hash]
56
+ def add_schema(schema_hash)
57
+ name = schema_hash['name']
58
+ namespace = schema_hash['namespace']
59
+ full_name = Avro::Name.make_fullname(name, namespace)
60
+ return if @schemas.key?(full_name)
61
+
62
+ # We pass in copy of @schemas which Avro can freely modify
63
+ # and register the sub-schema. It doesn't matter because
64
+ # we will discard it.
65
+ schema = Avro::Schema.real_parse(schema_hash, @schemas.dup)
66
+ @schemas[full_name] = schema
67
+ @schema_text[full_name] = JSON.pretty_generate(schema_hash)
68
+
69
+ schema
70
+ end
71
+
72
+ protected
73
+
74
+ # Loads single schema
75
+ # Such method is not thread-safe, do not call it of from mutex synchronization routine
76
+ def load_schema!(fullname, local_schemas_cache = {})
77
+ schema_path = build_schema_path(fullname)
78
+ schema_text = File.read(schema_path)
79
+ schema_json = JSON.parse(schema_text)
80
+
81
+ schema = Avro::Schema.real_parse(schema_json, local_schemas_cache)
82
+
83
+ # Don't cache the parsed schema until after its fullname is validated
84
+ if schema.respond_to?(:fullname) && schema.fullname != fullname
85
+ raise SchemaRegistry::SchemaError, "expected schema `#{schema_path}' to define type `#{fullname}'"
86
+ end
87
+
88
+ # Cache only this new top-level schema by its fullname. It's critical
89
+ # not to make every sub-schema resolvable at the top level here because
90
+ # multiple different avsc files may define the same sub-schema, and
91
+ # if we share the @schemas cache across all parsing contexts, the Avro
92
+ # gem will raise an Avro::SchemaParseError when parsing another avsc
93
+ # file that contains a subschema with the same fullname as one
94
+ # encountered previously in a different file:
95
+ # <Avro::SchemaParseError: The name "foo.bar" is already in use.>
96
+ # Essentially, the only schemas that should be resolvable in @schemas
97
+ # are those that have their own .avsc files on disk.
98
+ @schemas[fullname] = schema
99
+ @schema_text[fullname] = schema_text
100
+
101
+ schema
102
+ rescue ::Avro::UnknownSchemaError => e
103
+ # Try to first resolve a referenced schema from disk.
104
+ # If this is successful, the Avro gem will have mutated the
105
+ # local_schemas_cache, adding all the new schemas it found.
106
+ load_schema!(::Avro::Name.make_fullname(e.type_name, e.default_namespace), local_schemas_cache)
107
+
108
+ # Attempt to re-parse the original schema now that the dependency
109
+ # has been resolved and use the now-updated local_schemas_cache to
110
+ # pick up where we left off.
111
+ local_schemas_cache.delete(fullname)
112
+ # Ensure all sub-schemas are cleaned up to avoid conflicts when re-parsing
113
+ # schema.
114
+ local_schemas_cache.each_key do |schema_name|
115
+ local_schemas_cache.delete(schema_name) unless File.exist?(build_schema_path(schema_name))
116
+ end
117
+ load_schema!(fullname, @schemas.dup)
118
+ rescue Errno::ENOENT, Errno::ENAMETOOLONG
119
+ raise "could not find Avro schema at `#{schema_path}'"
120
+ end
121
+
122
+ def build_schema_path(fullname)
123
+ *namespace, schema_name = fullname.split('.')
124
+ File.join(@path, *namespace, "#{schema_name}.avsc")
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SchemaRegistry
4
+ class CachedConfluentSchemaRegistry
5
+ # @param upstream [SchemaRegistry::ConfluentSchemaRegistry]
6
+ def initialize(upstream)
7
+ @upstream = upstream
8
+ @schemas_by_id = {}
9
+ @ids_by_schema = {}
10
+ @versions_by_subject_and_id = {}
11
+ end
12
+
13
+ # Delegate the following methods to the upstream
14
+ %i[subject_versions schema_subject_versions].each do |name|
15
+ define_method(name) do |*args|
16
+ instance_variable_get(:@upstream).send(name, *args)
17
+ end
18
+ end
19
+
20
+ # @param id [Integer] the schema ID to fetch
21
+ # @return [String] the schema string stored in the registry for the given id
22
+ def fetch(id)
23
+ @schemas_by_id[id] ||= @upstream.fetch(id)
24
+ end
25
+
26
+ # @param id [Integer] the schema ID to fetch
27
+ # @param subject [String] the subject to fetch the version for
28
+ # @return [Integer, nil] the version of the schema for the given subject and id, or nil if not found
29
+ def fetch_version(id, subject)
30
+ key = [subject, id]
31
+ return @versions_by_subject_and_id[key] if @versions_by_subject_and_id[key]
32
+
33
+ results = @upstream.schema_subject_versions(id)
34
+ @versions_by_subject_and_id[key] = results&.find { |r| r['subject'] == subject }&.dig('version')
35
+ end
36
+
37
+ # @param subject [String] the subject to check
38
+ # @param schema [String] the schema text to check
39
+ # @return [Boolean] true if we know the schema has been registered for that subject.
40
+ def registered?(subject, schema)
41
+ @ids_by_schema[[subject, schema]] && !@ids_by_schema[[subject, schema]].empty?
42
+ end
43
+
44
+ # @param subject [String] the subject to register the schema under
45
+ # @param schema [String] the schema text to register
46
+ # @param references [Array<Hash>] optional references to other schemas
47
+ # @param schema_type [String]
48
+ def register(subject, schema, references: [], schema_type: 'PROTOBUF')
49
+ key = [subject, schema]
50
+
51
+ @ids_by_schema[key] ||= @upstream.register(subject,
52
+ schema,
53
+ references: references,
54
+ schema_type: schema_type)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'excon'
4
+
5
+ class SchemaRegistry
6
+ class ConfluentSchemaRegistry
7
+ CONTENT_TYPE = 'application/vnd.schemaregistry.v1+json'
8
+
9
+ def initialize( # rubocop:disable Metrics/ParameterLists
10
+ url,
11
+ schema_context: nil,
12
+ logger: Logger.new($stdout),
13
+ proxy: nil,
14
+ user: nil,
15
+ password: nil,
16
+ ssl_ca_file: nil,
17
+ client_cert: nil,
18
+ client_key: nil,
19
+ client_key_pass: nil,
20
+ client_cert_data: nil,
21
+ client_key_data: nil,
22
+ path_prefix: nil,
23
+ connect_timeout: nil,
24
+ resolv_resolver: nil,
25
+ retry_limit: nil
26
+ )
27
+ @path_prefix = path_prefix
28
+ @schema_context_prefix = schema_context.nil? ? '' : ":.#{schema_context}:"
29
+ @schema_context_options = schema_context.nil? ? {} : { query: { subject: @schema_context_prefix } }
30
+ @logger = logger
31
+ headers = Excon.defaults[:headers].merge(
32
+ 'Content-Type' => CONTENT_TYPE
33
+ )
34
+ params = {
35
+ headers: headers,
36
+ user: user,
37
+ password: password,
38
+ proxy: proxy,
39
+ ssl_ca_file: ssl_ca_file,
40
+ client_cert: client_cert,
41
+ client_key: client_key,
42
+ client_key_pass: client_key_pass,
43
+ client_cert_data: client_cert_data,
44
+ client_key_data: client_key_data,
45
+ resolv_resolver: resolv_resolver,
46
+ connect_timeout: connect_timeout,
47
+ retry_limit: retry_limit
48
+ }
49
+ # Remove nil params to allow Excon to use its default values
50
+ params.reject! { |_, v| v.nil? }
51
+ @connection = Excon.new(
52
+ url,
53
+ params
54
+ )
55
+ end
56
+
57
+ # @param id [Integer] the schema ID to fetch
58
+ # @return [String] the schema string stored in the registry for the given id
59
+ def fetch(id)
60
+ @logger.info "Fetching schema with id #{id}"
61
+ data = get("/schemas/ids/#{id}", idempotent: true, **@schema_context_options)
62
+ data.fetch('schema')
63
+ end
64
+
65
+ # @param schema_id [Integer] the schema ID to fetch versions for
66
+ # @return [Array<Hash>] an array of versions for the given schema ID
67
+ def schema_subject_versions(schema_id)
68
+ get("/schemas/ids/#{schema_id}/versions", idempotent: true, **@schema_context_options)
69
+ end
70
+
71
+ # @param subject [String] the subject to check
72
+ # @param schema [String] the schema text to check
73
+ # @param references [Array<Hash>] optional references to other schemas
74
+ # @return [Integer] the ID of the registered schema
75
+ def register(subject, schema, references: [], schema_type: 'PROTOBUF')
76
+ data = post("/subjects/#{@schema_context_prefix}#{CGI.escapeURIComponent(subject)}/versions",
77
+ body: { schemaType: schema_type,
78
+ references: references,
79
+ schema: schema.to_s }.to_json)
80
+
81
+ id = data.fetch('id')
82
+
83
+ @logger.info "Registered schema for subject `#{@schema_context_prefix}#{subject}`; id = #{id}"
84
+
85
+ id
86
+ end
87
+
88
+ # @param subject [String]
89
+ # @return [Array<Hash>] an array of versions for the given subject
90
+ def subject_versions(subject)
91
+ get("/subjects/#{@schema_context_prefix}#{CGI.escapeURIComponent(subject)}/versions", idempotent: true)
92
+ end
93
+
94
+ private
95
+
96
+ def get(path, **options)
97
+ request(path, method: :get, **options)
98
+ end
99
+
100
+ def put(path, **options)
101
+ request(path, method: :put, **options)
102
+ end
103
+
104
+ def post(path, **options)
105
+ request(path, method: :post, **options)
106
+ end
107
+
108
+ def request(path, **options)
109
+ options = { expects: 200 }.merge!(options)
110
+ path = File.join(@path_prefix, path) unless @path_prefix.nil?
111
+ response = @connection.request(path: path, **options)
112
+ JSON.parse(response.body)
113
+ rescue Excon::Error => e
114
+ @logger.error("Error while requesting #{path}: #{e.response.body}")
115
+ raise
116
+ end
117
+ end
118
+ end