rimless 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.editorconfig +30 -0
- data/.gitignore +15 -0
- data/.rspec +3 -0
- data/.rubocop.yml +53 -0
- data/.simplecov +3 -0
- data/.travis.yml +27 -0
- data/.yardopts +6 -0
- data/Appraisals +21 -0
- data/CHANGELOG.md +8 -0
- data/Dockerfile +28 -0
- data/Gemfile +8 -0
- data/LICENSE +21 -0
- data/Makefile +149 -0
- data/README.md +427 -0
- data/Rakefile +76 -0
- data/bin/console +16 -0
- data/bin/run +12 -0
- data/bin/setup +8 -0
- data/config/docker/.bash_profile +3 -0
- data/config/docker/.bashrc +49 -0
- data/config/docker/.inputrc +17 -0
- data/doc/assets/project.svg +68 -0
- data/docker-compose.yml +8 -0
- data/gemfiles/rails_4.2.gemfile +10 -0
- data/gemfiles/rails_5.0.gemfile +10 -0
- data/gemfiles/rails_5.1.gemfile +10 -0
- data/gemfiles/rails_5.2.gemfile +10 -0
- data/lib/rimless.rb +37 -0
- data/lib/rimless/avro_helpers.rb +46 -0
- data/lib/rimless/avro_utils.rb +96 -0
- data/lib/rimless/configuration.rb +57 -0
- data/lib/rimless/configuration_handling.rb +75 -0
- data/lib/rimless/dependencies.rb +55 -0
- data/lib/rimless/kafka_helpers.rb +106 -0
- data/lib/rimless/railtie.rb +25 -0
- data/lib/rimless/rspec.rb +40 -0
- data/lib/rimless/rspec/helpers.rb +17 -0
- data/lib/rimless/rspec/matchers.rb +286 -0
- data/lib/rimless/version.rb +6 -0
- data/rimless.gemspec +48 -0
- metadata +382 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d1bd00e69040a81a5a674978db28fb0e1cafe460b88fc9e7e0b6dd06ebc9de35
|
4
|
+
data.tar.gz: 822d8080bad4a11cf4bd938ca77ecdcc438cdcb91d6c61f957a4462364b352c2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b843f66b1bc118c71f43cc58b6c2b09415b6fc274fd9aae50d64bf44743b70fbfa4c847b0a8c717d0aff7bf77f6199909d467e174ff72dccbb9b3f3d4539730a
|
7
|
+
data.tar.gz: '015828b62edf47adb656ff097980810d946ab940d3bfb0ab78471103687c2db1430058a10f23176696c6acf7e3c59c38791f07baa944fcc9e4cc27bac7df81e6'
|
data/.editorconfig
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# http://editorconfig.org
|
2
|
+
root = true
|
3
|
+
|
4
|
+
[*]
|
5
|
+
indent_style = space
|
6
|
+
indent_size = 2
|
7
|
+
end_of_line = lf
|
8
|
+
charset = utf-8
|
9
|
+
trim_trailing_whitespace = true
|
10
|
+
insert_final_newline = true
|
11
|
+
|
12
|
+
[*.md]
|
13
|
+
trim_trailing_whitespace = true
|
14
|
+
|
15
|
+
[*.json]
|
16
|
+
indent_style = space
|
17
|
+
indent_size = 2
|
18
|
+
|
19
|
+
[*.yml]
|
20
|
+
indent_style = space
|
21
|
+
indent_size = 2
|
22
|
+
|
23
|
+
[Makefile]
|
24
|
+
trim_trailing_whitespace = true
|
25
|
+
indent_style = tab
|
26
|
+
indent_size = 4
|
27
|
+
|
28
|
+
[*.sh]
|
29
|
+
indent_style = space
|
30
|
+
indent_size = 2
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require: rubocop-rspec
|
2
|
+
|
3
|
+
Rails:
|
4
|
+
Enabled: true
|
5
|
+
|
6
|
+
Documentation:
|
7
|
+
Enabled: true
|
8
|
+
|
9
|
+
AllCops:
|
10
|
+
DisplayCopNames: true
|
11
|
+
TargetRubyVersion: 2.3
|
12
|
+
Exclude:
|
13
|
+
- bin/**/*
|
14
|
+
- vendor/**/*
|
15
|
+
- build/**/*
|
16
|
+
- gemfiles/vendor/**/*
|
17
|
+
|
18
|
+
Metrics/BlockLength:
|
19
|
+
Exclude:
|
20
|
+
- Rakefile
|
21
|
+
- '*.gemspec'
|
22
|
+
- spec/**/*.rb
|
23
|
+
- '**/*.rake'
|
24
|
+
- doc/**/*.rb
|
25
|
+
|
26
|
+
# Document all the things.
|
27
|
+
Style/DocumentationMethod:
|
28
|
+
Enabled: true
|
29
|
+
RequireForNonPublicMethods: true
|
30
|
+
|
31
|
+
# It's a deliberate idiom in RSpec.
|
32
|
+
# See: https://github.com/bbatsov/rubocop/issues/4222
|
33
|
+
Lint/AmbiguousBlockAssociation:
|
34
|
+
Exclude:
|
35
|
+
- "spec/**/*"
|
36
|
+
|
37
|
+
# Because +expect_any_instance_of().to have_received()+ is not
|
38
|
+
# supported with the +with(hash_including)+ matchers
|
39
|
+
RSpec/MessageSpies:
|
40
|
+
EnforcedStyle: receive
|
41
|
+
|
42
|
+
# Because nesting makes sense here to group the feature tests
|
43
|
+
# more effective. This increases maintainability.
|
44
|
+
RSpec/NestedGroups:
|
45
|
+
Max: 4
|
46
|
+
|
47
|
+
# Disable regular Rails spec paths.
|
48
|
+
RSpec/FilePath:
|
49
|
+
Enabled: false
|
50
|
+
|
51
|
+
# Because we just implemented the ActiveRecord API.
|
52
|
+
Rails/SkipsModelValidations:
|
53
|
+
Enabled: false
|
data/.simplecov
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
env:
|
2
|
+
global:
|
3
|
+
- CC_TEST_REPORTER_ID=f926dbf2ed89c7918e7a47f4f14f7d8386cc102d4cfa0a4e84051c6c976975ea
|
4
|
+
|
5
|
+
sudo: false
|
6
|
+
language: ruby
|
7
|
+
cache: bundler
|
8
|
+
rvm:
|
9
|
+
- 2.6
|
10
|
+
- 2.5
|
11
|
+
- 2.4
|
12
|
+
- 2.3
|
13
|
+
|
14
|
+
gemfile:
|
15
|
+
- gemfiles/rails_4.2.gemfile
|
16
|
+
- gemfiles/rails_5.0.gemfile
|
17
|
+
- gemfiles/rails_5.1.gemfile
|
18
|
+
- gemfiles/rails_5.2.gemfile
|
19
|
+
|
20
|
+
before_install: gem install bundler
|
21
|
+
before_script:
|
22
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
23
|
+
- chmod +x ./cc-test-reporter
|
24
|
+
- ./cc-test-reporter before-build
|
25
|
+
script: bundle exec rake
|
26
|
+
after_script:
|
27
|
+
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
|
data/.yardopts
ADDED
data/Appraisals
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
appraise 'rails-4.2' do
|
4
|
+
gem 'activesupport', '~> 4.2.11'
|
5
|
+
gem 'railties', '~> 4.2.11'
|
6
|
+
end
|
7
|
+
|
8
|
+
appraise 'rails-5.0' do
|
9
|
+
gem 'activesupport', '~> 5.0.7'
|
10
|
+
gem 'railties', '~> 5.0.7'
|
11
|
+
end
|
12
|
+
|
13
|
+
appraise 'rails-5.1' do
|
14
|
+
gem 'activesupport', '~> 5.1.6'
|
15
|
+
gem 'railties', '~> 5.1.6'
|
16
|
+
end
|
17
|
+
|
18
|
+
appraise 'rails-5.2' do
|
19
|
+
gem 'activesupport', '~> 5.2.2'
|
20
|
+
gem 'railties', '~> 5.2.2'
|
21
|
+
end
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
### 0.1.0
|
2
|
+
|
3
|
+
* The first release with support for simple Apache Avro message producing on
|
4
|
+
Apache Kafka/Confluent Schema Registry
|
5
|
+
* Improved the automatic Avro Schema ERB template compiling and included a JSON
|
6
|
+
validation for each file
|
7
|
+
* Added a powerful RSpec helper/matcher to ease message producer logic tests
|
8
|
+
* Added an extensive documentation
|
data/Dockerfile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
FROM hausgold/ruby:2.3
|
2
|
+
MAINTAINER Hermann Mayer <hermann.mayer@hausgold.de>
|
3
|
+
|
4
|
+
# Update system gem
|
5
|
+
RUN gem update --system
|
6
|
+
|
7
|
+
# Install system packages and the latest bundler
|
8
|
+
RUN apt-get update -yqqq && \
|
9
|
+
apt-get install -y \
|
10
|
+
build-essential locales sudo vim \
|
11
|
+
ca-certificates \
|
12
|
+
bash-completion inotify-tools && \
|
13
|
+
echo 'en_US.UTF-8 UTF-8' >> /etc/locale.gen && /usr/sbin/locale-gen && \
|
14
|
+
gem install bundler --no-document --no-prerelease
|
15
|
+
|
16
|
+
# Add new web user
|
17
|
+
RUN mkdir /app && \
|
18
|
+
adduser web --home /home/web --shell /bin/bash \
|
19
|
+
--disabled-password --gecos ""
|
20
|
+
COPY config/docker/* /home/web/
|
21
|
+
RUN chown web:web -R /app /home/web /usr/local/bundle && \
|
22
|
+
mkdir -p /home/web/.ssh
|
23
|
+
|
24
|
+
# Set the root password and grant root access to web
|
25
|
+
RUN echo 'root:root' | chpasswd
|
26
|
+
RUN echo 'web ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers
|
27
|
+
|
28
|
+
WORKDIR /app
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2019 HAUSGOLD | talocasa GmbH
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/Makefile
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
MAKEFLAGS += --warn-undefined-variables -j1
|
2
|
+
SHELL := bash
|
3
|
+
.SHELLFLAGS := -eu -o pipefail -c
|
4
|
+
.DEFAULT_GOAL := all
|
5
|
+
.DELETE_ON_ERROR:
|
6
|
+
.SUFFIXES:
|
7
|
+
.PHONY:
|
8
|
+
|
9
|
+
# Environment switches
|
10
|
+
MAKE_ENV ?= docker
|
11
|
+
COMPOSE_RUN_SHELL_FLAGS ?= --rm
|
12
|
+
|
13
|
+
# Directories
|
14
|
+
VENDOR_DIR ?= vendor/bundle
|
15
|
+
GEMFILES_DIR ?= gemfiles
|
16
|
+
|
17
|
+
# Host binaries
|
18
|
+
AWK ?= awk
|
19
|
+
BASH ?= bash
|
20
|
+
COMPOSE ?= docker-compose
|
21
|
+
DOCKER ?= docker
|
22
|
+
GREP ?= grep
|
23
|
+
ID ?= id
|
24
|
+
MKDIR ?= mkdir
|
25
|
+
RM ?= rm
|
26
|
+
XARGS ?= xargs
|
27
|
+
|
28
|
+
# Container binaries
|
29
|
+
BUNDLE ?= bundle
|
30
|
+
APPRAISAL ?= appraisal
|
31
|
+
RAKE ?= rake
|
32
|
+
YARD ?= yard
|
33
|
+
RAKE ?= rake
|
34
|
+
RUBOCOP ?= rubocop
|
35
|
+
|
36
|
+
# Files
|
37
|
+
GEMFILES ?= $(subst _,-,$(patsubst $(GEMFILES_DIR)/%.gemfile,%,\
|
38
|
+
$(wildcard $(GEMFILES_DIR)/*.gemfile)))
|
39
|
+
TEST_GEMFILES := $(GEMFILES:%=test-%)
|
40
|
+
|
41
|
+
# Define a generic shell run wrapper
|
42
|
+
# $1 - The command to run
|
43
|
+
ifeq ($(MAKE_ENV),docker)
|
44
|
+
define run-shell
|
45
|
+
$(COMPOSE) run $(COMPOSE_RUN_SHELL_FLAGS) \
|
46
|
+
-e LANG=en_US.UTF-8 -e LANGUAGE=en_US.UTF-8 -e LC_ALL=en_US.UTF-8 \
|
47
|
+
-e HOME=/home/web -e BUNDLE_APP_CONFIG=/app/.bundle \
|
48
|
+
-u `$(ID) -u` test bash -c 'sleep 0.1; echo; $(1)'
|
49
|
+
endef
|
50
|
+
else ifeq ($(MAKE_ENV),baremetal)
|
51
|
+
define run-shell
|
52
|
+
$(1)
|
53
|
+
endef
|
54
|
+
endif
|
55
|
+
|
56
|
+
all:
|
57
|
+
# rimless
|
58
|
+
#
|
59
|
+
# install Install the dependencies
|
60
|
+
# update Update the local Gemset dependencies
|
61
|
+
# clean Clean the dependencies
|
62
|
+
#
|
63
|
+
# test Run the whole test suite
|
64
|
+
# test-style Test the code styles
|
65
|
+
#
|
66
|
+
# docs Generate the Ruby documentation of the library
|
67
|
+
# stats Print the code statistics (library and test suite)
|
68
|
+
# notes Print all the notes from the code
|
69
|
+
# release Release a new Gem version (maintainers only)
|
70
|
+
#
|
71
|
+
# shell Run an interactive shell on the container
|
72
|
+
# shell-irb Run an interactive IRB shell on the container
|
73
|
+
|
74
|
+
install:
|
75
|
+
# Install the dependencies
|
76
|
+
@$(MKDIR) -p $(VENDOR_DIR)
|
77
|
+
@$(call run-shell,$(BUNDLE) check || $(BUNDLE) install --path $(VENDOR_DIR))
|
78
|
+
@$(call run-shell,$(BUNDLE) exec $(APPRAISAL) install)
|
79
|
+
|
80
|
+
update: install
|
81
|
+
# Install the dependencies
|
82
|
+
@$(MKDIR) -p $(VENDOR_DIR)
|
83
|
+
@$(call run-shell,$(BUNDLE) exec $(APPRAISAL) update)
|
84
|
+
|
85
|
+
test: #install
|
86
|
+
# Run the whole test suite
|
87
|
+
@$(call run-shell,$(BUNDLE) exec $(RAKE))
|
88
|
+
|
89
|
+
$(TEST_GEMFILES): GEMFILE=$(@:test-%=%)
|
90
|
+
$(TEST_GEMFILES):
|
91
|
+
# Run the whole test suite ($(GEMFILE))
|
92
|
+
@$(call run-shell,$(BUNDLE) exec $(APPRAISAL) $(GEMFILE) $(RAKE))
|
93
|
+
|
94
|
+
test-style: \
|
95
|
+
test-style-ruby
|
96
|
+
|
97
|
+
test-style-ruby:
|
98
|
+
# Run the static code analyzer (rubocop)
|
99
|
+
@$(call run-shell,$(BUNDLE) exec $(RUBOCOP) -a)
|
100
|
+
|
101
|
+
clean:
|
102
|
+
# Clean the dependencies
|
103
|
+
@$(RM) -rf $(VENDOR_DIR)
|
104
|
+
@$(RM) -rf $(VENDOR_DIR)/Gemfile.lock
|
105
|
+
@$(RM) -rf $(GEMFILES_DIR)/vendor
|
106
|
+
@$(RM) -rf $(GEMFILES_DIR)/*.lock
|
107
|
+
@$(RM) -rf pkg
|
108
|
+
@$(RM) -rf coverage
|
109
|
+
|
110
|
+
clean-containers:
|
111
|
+
# Clean running containers
|
112
|
+
ifeq ($(MAKE_ENV),docker)
|
113
|
+
@$(COMPOSE) down
|
114
|
+
endif
|
115
|
+
|
116
|
+
clean-images:
|
117
|
+
# Clean build images
|
118
|
+
ifeq ($(MAKE_ENV),docker)
|
119
|
+
@-$(DOCKER) images | $(GREP) hausgold-sdk \
|
120
|
+
| $(AWK) '{ print $$3 }' \
|
121
|
+
| $(XARGS) -rn1 $(DOCKER) rmi -f
|
122
|
+
endif
|
123
|
+
|
124
|
+
distclean: clean clean-containers clean-images
|
125
|
+
|
126
|
+
shell: install
|
127
|
+
# Run an interactive shell on the container
|
128
|
+
@$(call run-shell,$(BASH) -i)
|
129
|
+
|
130
|
+
shell-irb: install
|
131
|
+
# Run an interactive IRB shell on the container
|
132
|
+
@$(call run-shell,bin/console)
|
133
|
+
|
134
|
+
docs: install
|
135
|
+
# Build the API documentation
|
136
|
+
@$(call run-shell,$(BUNDLE) exec $(YARD) -q && \
|
137
|
+
$(BUNDLE) exec $(YARD) stats --list-undoc --compact)
|
138
|
+
|
139
|
+
notes: install
|
140
|
+
# Print the code statistics (library and test suite)
|
141
|
+
@$(call run-shell,$(BUNDLE) exec $(RAKE) notes)
|
142
|
+
|
143
|
+
stats: install
|
144
|
+
# Print all the notes from the code
|
145
|
+
@$(call run-shell,$(BUNDLE) exec $(RAKE) stats)
|
146
|
+
|
147
|
+
release:
|
148
|
+
# Release a new gem version
|
149
|
+
@$(RAKE) release
|
data/README.md
ADDED
@@ -0,0 +1,427 @@
|
|
1
|
+

|
2
|
+
|
3
|
+
[](https://travis-ci.com/hausgold/rimless)
|
4
|
+
[](https://badge.fury.io/rb/rimless)
|
5
|
+
[](https://codeclimate.com/repos/5cb06f700f7b09026e00a896/maintainability)
|
6
|
+
[](https://codeclimate.com/repos/5cb06f700f7b09026e00a896/test_coverage)
|
7
|
+
[](https://www.rubydoc.info/gems/rimless)
|
8
|
+
|
9
|
+
This project is dedicated to ship a ready to use [Apache
|
10
|
+
Kafka](https://kafka.apache.org/) / [Confluent Schema
|
11
|
+
Registry](https://docs.confluent.io/current/schema-registry/index.html) /
|
12
|
+
[Apache Avro](https://avro.apache.org/) message producing toolset by making use
|
13
|
+
of the [WaterDrop](https://rubygems.org/gems/waterdrop) and
|
14
|
+
[AvroTurf](https://rubygems.org/gems/avro_turf) gems. It comes as an
|
15
|
+
opinionated framework which sets up solid conventions for producing messages.
|
16
|
+
|
17
|
+
- [Installation](#installation)
|
18
|
+
- [Usage](#usage)
|
19
|
+
- [Configuration](#configuration)
|
20
|
+
- [Available environment variables](#available-environment-variables)
|
21
|
+
- [Conventions](#conventions)
|
22
|
+
- [Apache Kafka Topic](#apache-kafka-topic)
|
23
|
+
- [Confluent Schema Registry Subject](#confluent-schema-registry-subject)
|
24
|
+
- [Organize and write schema definitions](#organize-and-write-schema-definitions)
|
25
|
+
- [Producing messages](#producing-messages)
|
26
|
+
- [Handling of schemaless deep blobs](#handling-of-schemaless-deep-blobs)
|
27
|
+
- [Writing tests for your messages](#writing-tests-for-your-messages)
|
28
|
+
- [Development](#development)
|
29
|
+
- [Contributing](#contributing)
|
30
|
+
|
31
|
+
## Installation
|
32
|
+
|
33
|
+
Add this line to your application's Gemfile:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
gem 'rimless'
|
37
|
+
```
|
38
|
+
|
39
|
+
And then execute:
|
40
|
+
|
41
|
+
```bash
|
42
|
+
$ bundle
|
43
|
+
```
|
44
|
+
|
45
|
+
Or install it yourself as:
|
46
|
+
|
47
|
+
```bash
|
48
|
+
$ gem install rimless
|
49
|
+
```
|
50
|
+
|
51
|
+
## Usage
|
52
|
+
|
53
|
+
### Configuration
|
54
|
+
|
55
|
+
You can configure the rimless gem via an Rails initializer, by environment
|
56
|
+
variables or on demand. Here we show a common Rails initializer example:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
Rimless.configure do |conf|
|
60
|
+
# Defaults to +Rails.env+ when available
|
61
|
+
conf.env = 'test'
|
62
|
+
# Defaults to your Rails application name when available
|
63
|
+
conf.app_name = 'your-app'
|
64
|
+
# Dito
|
65
|
+
conf.client_id = 'your-app'
|
66
|
+
|
67
|
+
# Writes to stdout by default
|
68
|
+
conf.logger = Logger.new(IO::NULL)
|
69
|
+
|
70
|
+
# Defaults to the default Rails configuration directory,
|
71
|
+
# or the current working directory plus +avro_schemas+
|
72
|
+
conf.avro_schema_path = 'config/avro_schemas'
|
73
|
+
conf.compiled_avro_schema_path = 'config/avro_schemas/compiled'
|
74
|
+
|
75
|
+
# The list of Apache Kafka brokers for cluster discovery,
|
76
|
+
# set to HAUSGOLD defaults when not set
|
77
|
+
conf.kafka_brokers = 'kafka://your.domain:9092,kafka..'
|
78
|
+
|
79
|
+
# The Confluent Schema Registry API URL,
|
80
|
+
# set to HAUSGOLD defaults when not set
|
81
|
+
conf.schema_registry_url = 'http://your.schema-registry.local'
|
82
|
+
end
|
83
|
+
```
|
84
|
+
|
85
|
+
The rimless gem comes with sensitive defaults as you can see. For most users an
|
86
|
+
extra configuration is not needed.
|
87
|
+
|
88
|
+
#### Available environment variables
|
89
|
+
|
90
|
+
The rimless gem can be configured hardly with its configuration code block like
|
91
|
+
shown before. Respecting the [twelve-factor app](https://12factor.net/)
|
92
|
+
concerns, the gem allows you to set almost all configurations (just the
|
93
|
+
relevant ones for runtime) via environment variables. Here comes a list of
|
94
|
+
available configuration options:
|
95
|
+
|
96
|
+
* **KAFKA_ENV**: The application environment. Falls back to `Rails.env` when available.
|
97
|
+
* **KAFKA_CLIENT_ID**: The Apache Kafka client identifier, falls back the the local application name.
|
98
|
+
* **KAFKA_BROKERS**: A comma separated list of Apache Kafka brokers for cluster discovery (Plaintext, no-auth/no-SSL only for now) (eg. `kafka://your.domain:9092,kafka..`)
|
99
|
+
* **KAFKA_SCHEMA_REGISTRY_URL**: The Confluent Schema Registry API URL to use for schema registrations.
|
100
|
+
|
101
|
+
### Conventions
|
102
|
+
|
103
|
+
#### Apache Kafka Topic
|
104
|
+
|
105
|
+
The topic name on Kafka is prefixed with the
|
106
|
+
application environment and application name. This allows the usage of a single
|
107
|
+
Apache Kafka cluster for multiple application environments (eg. canary and
|
108
|
+
production). The application name on the topic allows direct knowledge of the
|
109
|
+
message origin. Convention rules:
|
110
|
+
|
111
|
+
* Schema is `<ENV>.<APP>.<CONCERN>`
|
112
|
+
* All components are lowercase and in [kebab-case](http://bit.ly/2IoQZiv) form
|
113
|
+
|
114
|
+
Here comes a Kafka topic name example: `production.identity-api.users`
|
115
|
+
|
116
|
+
#### Confluent Schema Registry Subject
|
117
|
+
|
118
|
+
Each subject (schema) is versioned and named for reference on the Schema
|
119
|
+
Registry. The subject naming convention is mostly the same as the Apache Kafka
|
120
|
+
Topic convention, except the allowed characters. [Apache
|
121
|
+
Avro](https://avro.apache.org/docs/1.8.2/spec.html#namespace) just allows
|
122
|
+
`[A-Za-z0-9_]` and no numbers on the first char. The application environment
|
123
|
+
prefix allows the usage of the very same Schema Registry instance for multipe
|
124
|
+
environments and the application name just reflects the schema origin.
|
125
|
+
Convention rules:
|
126
|
+
|
127
|
+
* Schema is `<ENV>.<APP>.<ENTITY>`
|
128
|
+
* All components are lowercase and in [snake_case](http://bit.ly/2IoQZiv) form
|
129
|
+
|
130
|
+
Here comes a subject name example: `production.identity_api.user_v1`
|
131
|
+
|
132
|
+
**Gotcha**: Why is this `user_v1` when the Schema Registry is tracking the
|
133
|
+
subject versions all by itself? At HAUSGOLD we stick to our API definition
|
134
|
+
versions of our entity representations. So a users v1 API looks like the
|
135
|
+
`user_v1` schema definition, this eases interoperability. The rimless gem does
|
136
|
+
not force you to do so as well.
|
137
|
+
|
138
|
+
### Organize and write schema definitions
|
139
|
+
|
140
|
+
Just because you want to produce messages with rimless, it comes to the point
|
141
|
+
that you need to [define your data
|
142
|
+
schemas](https://avro.apache.org/docs/1.8.2/spec.html). The rimless gem
|
143
|
+
supports you with some good conventions, automatic compilation of Apache Avro
|
144
|
+
schema [ERB
|
145
|
+
templates](https://ruby-doc.org/stdlib-2.6.2/libdoc/erb/rdoc/ERB.html) and
|
146
|
+
painless JSON validation of them.
|
147
|
+
|
148
|
+
First things first, by convention the rimless gem looks for Apache Avro schema
|
149
|
+
ERB templates on the `$(pwd)/config/avro_schemas` directory. Nothing special
|
150
|
+
from the Rails perspective. You can also reconfigure the file locations, just
|
151
|
+
[see the configuration
|
152
|
+
block](https://github.com/hausgold/rimless/blob/master/lib/rimless/configuration.rb#L36).
|
153
|
+
|
154
|
+
Each schema template MUST end with the `.avsc.erb` extension to be picked up,
|
155
|
+
even in recursive directory structures. You can make use of the ERB templating
|
156
|
+
or not, but rimless just looks for these templates. When it comes to
|
157
|
+
structuring the Avro Schemas it is important that the file path reflects the
|
158
|
+
embeded schema namespace correctly. So when `$(pwd)/config/avro_schemas` is our
|
159
|
+
schema namespace root, then the `production.identity_api.user_v1` schema
|
160
|
+
converts to the
|
161
|
+
`$(pwd)/config/avro_schemas/compiled/production/identity_api/user_v1.avsc`
|
162
|
+
file path for Apache Avro.
|
163
|
+
|
164
|
+
The corresponding Avro Schema template is located at
|
165
|
+
`$(pwd)/config/avro_schemas/identity_api/user_v1.avsc.erb`. Now it's going to
|
166
|
+
be fancy. The automatic schema compiler picks up the dynamically/runtime set
|
167
|
+
namespace from the schema definition and converts it to its respective
|
168
|
+
directory structure. So when you boot your application container/instance
|
169
|
+
inside your *canary* environment, the schemas/messages should reflect this so
|
170
|
+
they do not mix with other environments.
|
171
|
+
|
172
|
+
Example time. **$(pwd)/config/avro_schemas/identity_api/user_v1.avsc.erb**:
|
173
|
+
|
174
|
+
```json
|
175
|
+
{
|
176
|
+
"name": "user_v1",
|
177
|
+
"namespace": "<%= namespace %>",
|
178
|
+
"type": "record",
|
179
|
+
"fields": [
|
180
|
+
{
|
181
|
+
"name": "firstname",
|
182
|
+
"type": "string"
|
183
|
+
},
|
184
|
+
{
|
185
|
+
"name": "lastname",
|
186
|
+
"type": "string"
|
187
|
+
},
|
188
|
+
{
|
189
|
+
"name": "address",
|
190
|
+
"type": "<%= namespace %>.address_v1"
|
191
|
+
},
|
192
|
+
{
|
193
|
+
"name": "metadata",
|
194
|
+
"doc": "Watch out for schemaless deep hash blobs. (+.avro_schemaless_h+)",
|
195
|
+
"type": {
|
196
|
+
"type": "map",
|
197
|
+
"values": "string"
|
198
|
+
}
|
199
|
+
}
|
200
|
+
]
|
201
|
+
}
|
202
|
+
```
|
203
|
+
|
204
|
+
**$(pwd)/config/avro_schemas/identity_api/address_v1.avsc.erb**:
|
205
|
+
|
206
|
+
```json
|
207
|
+
{
|
208
|
+
"name": "address_v1",
|
209
|
+
"namespace": "<%= namespace %>",
|
210
|
+
"type": "record",
|
211
|
+
"fields": [
|
212
|
+
{
|
213
|
+
"name": "street",
|
214
|
+
"type": "string"
|
215
|
+
}
|
216
|
+
{
|
217
|
+
"name": "city",
|
218
|
+
"type": "string"
|
219
|
+
}
|
220
|
+
]
|
221
|
+
}
|
222
|
+
```
|
223
|
+
|
224
|
+
The compiled Avro Schemas are written to the
|
225
|
+
`$(pwd)/config/avro_schemas/compiled/` directory by default. You can
|
226
|
+
[reconfigure the
|
227
|
+
location](https://github.com/hausgold/rimless/blob/master/lib/rimless/configuration.rb#L44)
|
228
|
+
if needed. For VCS systems like Git it is useful to create an relative ignore
|
229
|
+
list at `$(pwd)/config/avro_schemas/.gitignore` with the following contents:
|
230
|
+
|
231
|
+
```gitignore
|
232
|
+
compiled/
|
233
|
+
```
|
234
|
+
|
235
|
+
### Producing messages
|
236
|
+
|
237
|
+
Under the hood the rimless gem makes use of the [WaterDrop
|
238
|
+
gem](https://rubygems.org/gems/waterdrop) to send messages to the Apache Kafka
|
239
|
+
cluster. But with the addition to send Apache Avro encoded messages with a
|
240
|
+
single call. Here comes some examples how to use it:
|
241
|
+
|
242
|
+
```ruby
|
243
|
+
metadata = { hobbies: %w(dancing singing sports) }
|
244
|
+
address = { street: 'Bahnhofstraße 5-6', city: '12305 Berlin' }
|
245
|
+
user = { firstname: 'John', lastname: 'Doe',
|
246
|
+
address: address, metadata: Rimless.avro_schemaless_h(metadata) }
|
247
|
+
|
248
|
+
# Encode and send the message to a Kafka topic (sync, blocking)
|
249
|
+
Rimless.message(data: user, schema: :user_v1, topic: :users)
|
250
|
+
# schema is relative resolved to: +development.identity_api.user_v1+
|
251
|
+
# topic is relative resolved to: +development.identity-api.users+
|
252
|
+
|
253
|
+
# You can also make use of an asynchronous message sending
|
254
|
+
Rimless.async_message(data: user, schema: :user_v1, topic: :users)
|
255
|
+
|
256
|
+
# In cases you just want the encoded Apache Avro binary blob, you can encode it
|
257
|
+
# directly via the AvroTurf gem
|
258
|
+
encoded = Rimless.avro.encode(user, schema_name: 'user_v1')
|
259
|
+
|
260
|
+
# You can also send raw messages with the rimless gem, so encoding of your
|
261
|
+
# message must be done before
|
262
|
+
Rimless.raw_message(data: encoded, topic: :users)
|
263
|
+
# topic is relative resolved to: +development.identity-api.users+
|
264
|
+
|
265
|
+
# In case you want to send messages to a non-local application topic you can
|
266
|
+
# specify the application, too. This allows you to send a message to the
|
267
|
+
# +<ENV>.address-api.addresses+ from you local identity-api.
|
268
|
+
Rimless.raw_message(data: encoded, topic: { name: :users, app: 'address-api' })
|
269
|
+
# Also works with the Apache Avro encoding variant
|
270
|
+
Rimless.message(data: user, schema: :user_v1,
|
271
|
+
topic: { name: :users, app: 'address-api' })
|
272
|
+
|
273
|
+
# And for the sake of completeness, you can also send raw
|
274
|
+
# messages asynchronously
|
275
|
+
Rimless.async_raw_message(data: encoded, topic: :users)
|
276
|
+
```
|
277
|
+
|
278
|
+
#### Handling of schemaless deep blobs
|
279
|
+
|
280
|
+
Apache Avro is by design a strict, type casted format which does not allow
|
281
|
+
undefined mix and matching of deep structures. This is fine because it forces
|
282
|
+
the producer to think twice about the schema definition. But sometimes there is
|
283
|
+
unstructured data inside of entities. Think of a metadata hash on a user entity
|
284
|
+
were the user (eg. a frontend client) just can add whatever comes to his mind
|
285
|
+
for later processing. Its not searchable, its never touched by the backend, but
|
286
|
+
its present.
|
287
|
+
|
288
|
+
Thats a case we're experienced and kind of solved on the rimless gem. You can
|
289
|
+
make use of the `Rimless.avro_schemaless_h` method to [sparsify the data
|
290
|
+
recursively](https://github.com/simplymeasured/sparsify). Say you have the
|
291
|
+
following metadata hash:
|
292
|
+
|
293
|
+
```ruby
|
294
|
+
metadata = {
|
295
|
+
test: true,
|
296
|
+
hobbies: %w(writing cooking moshpit),
|
297
|
+
a: {
|
298
|
+
b: [
|
299
|
+
{ c: true },
|
300
|
+
{ d: false }
|
301
|
+
]
|
302
|
+
}
|
303
|
+
}
|
304
|
+
```
|
305
|
+
|
306
|
+
It's messy, by design. From the Apache Avro perspective you just can define a
|
307
|
+
map. The map keys are assumed to be strings - and the most hitting value data
|
308
|
+
type is a string, too. Thats where hash sparsification comes in. The resulting
|
309
|
+
metadata hash looks like this and can be encoded by Apache Avro:
|
310
|
+
|
311
|
+
```ruby
|
312
|
+
Rimless.avro_schemaless_h(metadata)
|
313
|
+
# => {
|
314
|
+
# "test"=>"true",
|
315
|
+
# "hobbies.0"=>"writing",
|
316
|
+
# "hobbies.1"=>"cooking",
|
317
|
+
# "hobbies.2"=>"moshpit",
|
318
|
+
# "a.b.0.c"=>"true",
|
319
|
+
# "a.b.1.d"=>"false"
|
320
|
+
# }
|
321
|
+
```
|
322
|
+
|
323
|
+
With the help of the [sparsify gem](https://rubygems.org/gems/sparsify) you can
|
324
|
+
also revert this to its original form. But with the loss of data type
|
325
|
+
correctness. Another approach can be used for these kind of scenarios: encoding
|
326
|
+
the schemaless data with JSON and just set the metadata field on the Apache
|
327
|
+
Avro schema to be a string. Choice is yours.
|
328
|
+
|
329
|
+
### Writing tests for your messages
|
330
|
+
|
331
|
+
Producing messages is a bliss with the rimless gem, but producing code needs to
|
332
|
+
be tested as well. Thats why the gem ships some RSpec helpers and matchers for
|
333
|
+
this purpose. A common situation is also handled by the RSpec extension: on the
|
334
|
+
test environment (eg. a continuous integration service) its not likely to have
|
335
|
+
a Apache Kafka/Confluent Schema Registry cluster available. Thats why actual
|
336
|
+
calls to Kafka/Schema Registry are mocked away.
|
337
|
+
|
338
|
+
First of all, just add `require 'rimless/rspec'` to your `spec_helper.rb` or
|
339
|
+
`rails_helper.rb`.
|
340
|
+
|
341
|
+
The `#avro_parse` helper is just in place to decode Apache Avro binary blobs to
|
342
|
+
their respective Ruby representations, in case you have to handle content
|
343
|
+
checks. Here comes an example:
|
344
|
+
|
345
|
+
```ruby
|
346
|
+
describe 'message content' do
|
347
|
+
let(:message) { file_fixture('user_v1_avro.bin').read }
|
348
|
+
|
349
|
+
it 'contains the firstname' do
|
350
|
+
expect(avro_parse(message)).to include(firstname: 'John')
|
351
|
+
end
|
352
|
+
end
|
353
|
+
```
|
354
|
+
|
355
|
+
Nothing special, not really fancy. A more complex situation occurs when you
|
356
|
+
separate your Kafka message producing logic inside an asynchronous job (eg.
|
357
|
+
Sidekiq or ActiveJob). Therefore is the `have_sent_kafka_message` matcher
|
358
|
+
available. Example time:
|
359
|
+
|
360
|
+
```ruby
|
361
|
+
describe 'message producer job' do
|
362
|
+
let(:user) { create(:user) } # FactoryBot FTW
|
363
|
+
let(:action) { SendUserCreatedMessageJob.perform_now(user) }
|
364
|
+
|
365
|
+
it 'encodes the message with the correct schema' do
|
366
|
+
expect { action }.to have_sent_kafka_message('test.identity_api.user_v1')
|
367
|
+
# the schema name --^
|
368
|
+
end
|
369
|
+
|
370
|
+
it 'sends a single message' do
|
371
|
+
expect { action }.to have_sent_kafka_message.exactly(1)
|
372
|
+
# Also available: (known from rspec-rails ActiveJob matcher)
|
373
|
+
# .at_least(2).times
|
374
|
+
# .at_most(3).times
|
375
|
+
# .exactly(:twice)
|
376
|
+
# .once
|
377
|
+
end
|
378
|
+
|
379
|
+
it 'sends the message to the correct topic' do
|
380
|
+
expect { action }.to \
|
381
|
+
have_sent_kafka_message.with(topic: 'test.identity-api.users')
|
382
|
+
end
|
383
|
+
|
384
|
+
it 'sends a message key' do
|
385
|
+
# Rimless.message(data: user, schema: :user_v1, topic: :users,
|
386
|
+
# key: user.id, partition: 1) # <-- additional Kafka metas
|
387
|
+
# @see https://github.com/karafka/waterdrop#usage for all options
|
388
|
+
expect { action }.to \
|
389
|
+
have_sent_kafka_message.with(key: String, topic: anything)
|
390
|
+
# mind the order --^
|
391
|
+
# its a argument list validation, all keys must be named
|
392
|
+
end
|
393
|
+
|
394
|
+
it 'sends the correct user data' do
|
395
|
+
expect { action }.to have_sent_kafka_message.with_data(firstname: 'John')
|
396
|
+
# deep hash including the given keys? --^
|
397
|
+
end
|
398
|
+
|
399
|
+
it 'sends no message (when not called)' do
|
400
|
+
expect { nil }.not_to have_sent_kafka_message
|
401
|
+
end
|
402
|
+
|
403
|
+
it 'allows complex expactations' do
|
404
|
+
expect { action; action }.to \
|
405
|
+
have_sent_kafka_message('test.identity_api.user_v1')
|
406
|
+
.with(key: user.id, topic: 'test.identity-api.users').twice
|
407
|
+
.with_data(firstname: 'John', lastname: 'Doe').twice
|
408
|
+
end
|
409
|
+
end
|
410
|
+
```
|
411
|
+
|
412
|
+
## Development
|
413
|
+
|
414
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
415
|
+
`bundle exec rake spec` to run the tests. You can also run `bin/console` for an
|
416
|
+
interactive prompt that will allow you to experiment.
|
417
|
+
|
418
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To
|
419
|
+
release a new version, update the version number in `version.rb`, and then run
|
420
|
+
`bundle exec rake release`, which will create a git tag for the version, push
|
421
|
+
git commits and tags, and push the `.gem` file to
|
422
|
+
[rubygems.org](https://rubygems.org).
|
423
|
+
|
424
|
+
## Contributing
|
425
|
+
|
426
|
+
Bug reports and pull requests are welcome on GitHub at
|
427
|
+
https://github.com/hausgold/rimless.
|