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 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.