rimless 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/api/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /vendor/
10
+ /gemfiles/vendor/
11
+ /Gemfile.lock
12
+ *.gemfile.lock
13
+
14
+ # rspec failure tracking
15
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
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
@@ -0,0 +1,3 @@
1
+ SimpleCov.start 'test_frameworks' do
2
+ add_filter '/vendor/bundle/'
3
+ end
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
@@ -0,0 +1,6 @@
1
+ --output-dir=doc/api
2
+ --plugin activesupport-concern
3
+ --markup=rdoc
4
+ -
5
+ README.md
6
+ lib/**/*.rb
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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in grape-jwt-authentication.gemspec
8
+ gemspec
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
+ ![rimless](doc/assets/project.svg)
2
+
3
+ [![Build Status](https://travis-ci.com/hausgold/rimless.svg?token=4XcyqxxmkyBSSV3wWRt7&branch=master)](https://travis-ci.com/hausgold/rimless)
4
+ [![Gem Version](https://badge.fury.io/rb/rimless.svg)](https://badge.fury.io/rb/rimless)
5
+ [![Maintainability](https://api.codeclimate.com/v1/badges/0d51996b52def6cf0262/maintainability)](https://codeclimate.com/repos/5cb06f700f7b09026e00a896/maintainability)
6
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/0d51996b52def6cf0262/test_coverage)](https://codeclimate.com/repos/5cb06f700f7b09026e00a896/test_coverage)
7
+ [![API docs](https://img.shields.io/badge/docs-API-blue.svg)](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.