polyn 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +4 -2
- data/README.md +36 -6
- data/lib/polyn/configuration.rb +3 -0
- data/lib/polyn/naming.rb +21 -0
- data/lib/polyn/nats/msg.rb +71 -0
- data/lib/polyn/nats.rb +23 -0
- data/lib/polyn/pull_subscriber.rb +3 -8
- data/lib/polyn/schema_store.rb +50 -23
- data/lib/polyn/serializers/json.rb +25 -25
- data/lib/polyn/testing/mock_jetstream.rb +30 -0
- data/lib/polyn/testing/mock_msg.rb +28 -0
- data/lib/polyn/testing/mock_nats.rb +56 -0
- data/lib/polyn/testing/mock_pull_subscription.rb +43 -0
- data/lib/polyn/testing.rb +31 -0
- data/lib/polyn/version.rb +1 -1
- data/lib/polyn.rb +104 -64
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 10ceff0b77d79960f425d6d7d6d1502095ed2b04533e9bd9fa5d37756f510e18
|
4
|
+
data.tar.gz: db2a410f4aa57312f72b7882c537a15d055fac0611f7eb7f2b31b3d5bf38a27e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e3b6c51512bcd60cf9276b69d5559f23d057625583832e74c40d271dc5cadbea82891bc41a053eb2d2695bcd374c65e2fe65f30e032acd8974cac0b62b9440d0
|
7
|
+
data.tar.gz: 879c217506428483c0842f306dac110b94da2188cbb57cf7c68bfaba6c5db11dd9a9f1fce86bc50f95d527fc613a6366ef0ff1d51268a1d30aceb83f9a23d050
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
polyn (0.
|
4
|
+
polyn (0.2.0)
|
5
5
|
json_schemer (~> 0.2)
|
6
6
|
nats-pure (~> 2.0)
|
7
7
|
yard (~> 0.9)
|
@@ -62,7 +62,9 @@ GEM
|
|
62
62
|
timecop (0.9.4)
|
63
63
|
unicode-display_width (2.1.0)
|
64
64
|
uri_template (0.7.0)
|
65
|
-
|
65
|
+
webrick (1.7.0)
|
66
|
+
yard (0.9.28)
|
67
|
+
webrick (~> 1.7.0)
|
66
68
|
|
67
69
|
PLATFORMS
|
68
70
|
arm64-darwin-20
|
data/README.md
CHANGED
@@ -66,22 +66,25 @@ require "nats/client"
|
|
66
66
|
require "polyn"
|
67
67
|
|
68
68
|
nats = NATS.connect
|
69
|
+
polyn = Polyn.connect(nats)
|
69
70
|
|
70
|
-
|
71
|
+
polyn.publish("user.created.v1", { name: "Mary" })
|
71
72
|
```
|
72
73
|
|
73
74
|
Add `:source` to make the `source` of the event more specific
|
74
75
|
|
75
76
|
|
76
77
|
```ruby
|
77
|
-
|
78
|
+
polyn.publish("user.created.v1", { name: "Mary" }, source: "new.users")
|
78
79
|
```
|
79
80
|
|
80
81
|
Add `:triggered_by` to add a triggering event to the `polyntrace`
|
81
82
|
|
82
83
|
```ruby
|
84
|
+
polyn = Polyn.connect(nats)
|
85
|
+
|
83
86
|
event = Polyn::Event.new
|
84
|
-
|
87
|
+
polyn.publish("user.created.v1", { name: "Mary" }, triggered_by: event)
|
85
88
|
```
|
86
89
|
|
87
90
|
You can also include options of `:header` and/or `:reply_to` to passthrough to NATS
|
@@ -93,8 +96,9 @@ require "nats/client"
|
|
93
96
|
require "polyn"
|
94
97
|
|
95
98
|
nats = NATS.connect
|
99
|
+
polyn = Polyn.connect(nats)
|
96
100
|
|
97
|
-
psub = Polyn.pull_subscribe(
|
101
|
+
psub = Polyn.pull_subscribe("user.created.v1")
|
98
102
|
|
99
103
|
loop do
|
100
104
|
msgs = psub.fetch(5)
|
@@ -116,8 +120,9 @@ require "nats/client"
|
|
116
120
|
require "polyn"
|
117
121
|
|
118
122
|
nats = NATS.connect
|
123
|
+
polyn = Polyn.connect
|
119
124
|
|
120
|
-
sub =
|
125
|
+
sub = polyn.subscribe("user.created.v1") { |msg| puts msg.data }
|
121
126
|
```
|
122
127
|
|
123
128
|
`Polyn.subscribe` will process the block you pass it asynchronously in a separate thread
|
@@ -131,10 +136,35 @@ require "nats/client"
|
|
131
136
|
|
132
137
|
nats = NATS.connect
|
133
138
|
nats.on_error { |e| raise e }
|
139
|
+
polyn = Polyn.connect(nats)
|
134
140
|
|
135
|
-
sub =
|
141
|
+
sub = polyn.subscribe("user.created.v1") { |msg| puts msg.data }
|
136
142
|
```
|
137
143
|
|
144
|
+
## Testing
|
145
|
+
|
146
|
+
### Setup
|
147
|
+
|
148
|
+
Set an environment variable of `POLYN_ENV=test` or `RAILS_ENV=test`.
|
149
|
+
|
150
|
+
Add the following to your `spec_helper.rb`
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
require "polyn/testing"
|
154
|
+
|
155
|
+
Polyn::Testing.setup
|
156
|
+
```
|
157
|
+
|
158
|
+
Add the following to individual test files `include_context :polyn`
|
159
|
+
|
160
|
+
### Test Isolation
|
161
|
+
|
162
|
+
Following the test setup instructions replaces *most* `Polyn` calls to NATS with mocks. Rather than hitting a real nats-server, the mocks will create an isolated sandbox for each test to ensure that message passing in one test is not affecting any other test. This will help prevent flaky tests and race conditions. It also makes concurrent testing possible. The tests will also all share the same schema store so that schemas aren't fetched from the nats-server repeatedly.
|
163
|
+
|
164
|
+
Despite mocking some NATS functionality you will still need a running nats-server for your testing.
|
165
|
+
When the tests start it will load all your schemas. The tests themselves will also use the running server to verify
|
166
|
+
stream and consumer configuration information. This hybrid mocking approach is intended to give isolation and reliability while also ensuring correct integration.
|
167
|
+
|
138
168
|
## Development
|
139
169
|
|
140
170
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
data/lib/polyn/configuration.rb
CHANGED
@@ -7,8 +7,11 @@ module Polyn
|
|
7
7
|
def initialize
|
8
8
|
@domain = nil
|
9
9
|
@source_root = nil
|
10
|
+
@polyn_env = ENV["POLYN_ENV"] || ENV["RAILS_ENV"] || "development"
|
10
11
|
end
|
11
12
|
|
13
|
+
attr_reader :polyn_env
|
14
|
+
|
12
15
|
def domain
|
13
16
|
@domain ||= Polyn::Naming.validate_domain_name!(@domain)
|
14
17
|
end
|
data/lib/polyn/naming.rb
CHANGED
@@ -93,6 +93,27 @@ module Polyn
|
|
93
93
|
end
|
94
94
|
end
|
95
95
|
|
96
|
+
##
|
97
|
+
# Determine if a given subject matches a subscription pattern
|
98
|
+
def self.subject_matches?(subject, pattern)
|
99
|
+
separator = "."
|
100
|
+
|
101
|
+
pattern_tokens = pattern.split(separator).map { |token| build_subject_pattern_part(token) }
|
102
|
+
pattern_tokens = pattern_tokens.join("\\#{separator}")
|
103
|
+
subject.match?(Regexp.new(pattern_tokens))
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.build_subject_pattern_part(token)
|
107
|
+
single_wildcard = "*"
|
108
|
+
multiple_wildcard = ">"
|
109
|
+
|
110
|
+
return "(\\w+)" if token == single_wildcard
|
111
|
+
|
112
|
+
return "((\\w+\\.)*\\w)" if token == multiple_wildcard
|
113
|
+
|
114
|
+
token
|
115
|
+
end
|
116
|
+
|
96
117
|
def self.domain
|
97
118
|
Polyn.configuration.domain
|
98
119
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Polyn
|
4
|
+
class Nats
|
5
|
+
##
|
6
|
+
# Wrapper around Nats::Msg so that we own it
|
7
|
+
class Msg
|
8
|
+
def initialize(msg)
|
9
|
+
@msg = msg
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_accessor :subject, :reply, :data, :header
|
13
|
+
|
14
|
+
def subject
|
15
|
+
@msg.subject
|
16
|
+
end
|
17
|
+
|
18
|
+
def reply
|
19
|
+
@msg.reply
|
20
|
+
end
|
21
|
+
|
22
|
+
def data
|
23
|
+
@msg.data
|
24
|
+
end
|
25
|
+
|
26
|
+
def data=(data)
|
27
|
+
@msg.data = data
|
28
|
+
end
|
29
|
+
|
30
|
+
def header
|
31
|
+
@msg.header
|
32
|
+
end
|
33
|
+
|
34
|
+
def ack(**params)
|
35
|
+
@msg.ack(**params)
|
36
|
+
end
|
37
|
+
|
38
|
+
def ack_sync(**params)
|
39
|
+
@msg.ack_sync(**params)
|
40
|
+
end
|
41
|
+
|
42
|
+
def nak(**params)
|
43
|
+
@msg.nack(**params)
|
44
|
+
end
|
45
|
+
|
46
|
+
def term(**params)
|
47
|
+
@msg.term(**params)
|
48
|
+
end
|
49
|
+
|
50
|
+
def in_progress(**params)
|
51
|
+
@msg.in_progress(**params)
|
52
|
+
end
|
53
|
+
|
54
|
+
def metadata
|
55
|
+
@msg.metadata
|
56
|
+
end
|
57
|
+
|
58
|
+
def respond(data = "")
|
59
|
+
@msg.respond(data)
|
60
|
+
end
|
61
|
+
|
62
|
+
def respond_msg(msg)
|
63
|
+
@msg.respond_msg(msg)
|
64
|
+
end
|
65
|
+
|
66
|
+
def inspect
|
67
|
+
@msg.inspect
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/lib/polyn/nats.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Polyn
|
4
|
+
##
|
5
|
+
# Adapter/wrapper around Nats
|
6
|
+
class Nats
|
7
|
+
def initialize(nats)
|
8
|
+
@nats = nats
|
9
|
+
end
|
10
|
+
|
11
|
+
def publish(type, json, reply, **opts)
|
12
|
+
@nats.publish(type, json, reply, **opts)
|
13
|
+
end
|
14
|
+
|
15
|
+
def subscribe(type, opts = {}, &callback)
|
16
|
+
@nats.subscribe(type, opts) { |msg| callback.call(Polyn::Nats::Msg.new(msg)) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def jetstream
|
20
|
+
@jetstream ||= @nats.jetstream
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -17,7 +17,7 @@ module Polyn
|
|
17
17
|
@stream = @nats.jetstream.find_stream_name_by_subject(@type)
|
18
18
|
self.class.validate_consumer_exists!(@nats, @stream, @consumer_name)
|
19
19
|
@psub = @nats.jetstream.pull_subscribe(@type, @consumer_name)
|
20
|
-
@
|
20
|
+
@serializer = fields.fetch(:serializer)
|
21
21
|
end
|
22
22
|
|
23
23
|
# nats-pure will create a consumer if the one you passed does not exist.
|
@@ -41,7 +41,8 @@ module Polyn
|
|
41
41
|
def fetch(batch = 1, params = {})
|
42
42
|
msgs = @psub.fetch(batch, params)
|
43
43
|
msgs.map do |msg|
|
44
|
-
|
44
|
+
msg = Polyn::Nats::Msg.new(msg)
|
45
|
+
event = @serializer.deserialize(msg.data)
|
45
46
|
if event.is_a?(Polyn::Errors::Error)
|
46
47
|
msg.term
|
47
48
|
raise event
|
@@ -51,11 +52,5 @@ module Polyn
|
|
51
52
|
msg
|
52
53
|
end
|
53
54
|
end
|
54
|
-
|
55
|
-
private
|
56
|
-
|
57
|
-
def store_name(opts)
|
58
|
-
opts.fetch(:store_name, Polyn::SchemaStore.store_name)
|
59
|
-
end
|
60
55
|
end
|
61
56
|
end
|
data/lib/polyn/schema_store.rb
CHANGED
@@ -4,47 +4,74 @@ module Polyn
|
|
4
4
|
##
|
5
5
|
# Persisting and interacting with persisted schemas
|
6
6
|
class SchemaStore
|
7
|
-
STORE_NAME
|
7
|
+
STORE_NAME = "POLYN_SCHEMAS"
|
8
|
+
|
9
|
+
def initialize(nats, **opts)
|
10
|
+
@nats = nats
|
11
|
+
@store_name = opts[:name] || STORE_NAME
|
12
|
+
@key_prefix = "$KV.#{@store_name}"
|
13
|
+
@schemas = opts[:schemas] || fetch_schemas
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :schemas
|
8
17
|
|
9
18
|
##
|
10
19
|
# Persist a schema. In prod/dev schemas should have already been persisted via
|
11
20
|
# the Polyn CLI.
|
12
|
-
def
|
21
|
+
def save(type, schema)
|
13
22
|
json_schema?(schema)
|
14
|
-
|
15
|
-
kv.put(type, JSON.generate(schema))
|
23
|
+
schemas[type] = JSON.generate(schema)
|
16
24
|
end
|
17
25
|
|
18
|
-
def
|
26
|
+
def json_schema?(schema)
|
19
27
|
JSONSchemer.schema(schema)
|
20
28
|
end
|
21
29
|
|
22
|
-
def
|
23
|
-
result = get(
|
30
|
+
def get!(type)
|
31
|
+
result = get(type)
|
24
32
|
raise result if result.is_a?(Polyn::Errors::SchemaError)
|
25
33
|
|
26
34
|
result
|
27
35
|
end
|
28
36
|
|
29
|
-
def
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
37
|
+
def get(type)
|
38
|
+
schema = schemas[type]
|
39
|
+
|
40
|
+
if schema.nil?
|
41
|
+
return Polyn::Errors::SchemaError.new(
|
42
|
+
"Schema for #{type} does not exist. Make sure it's "\
|
43
|
+
"been added to your `events` codebase and has been loaded "\
|
44
|
+
"into the schema store on your NATS server",
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
schema
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def fetch_schemas
|
54
|
+
loop_store_keys
|
38
55
|
rescue NATS::JetStream::Error::NotFound
|
39
|
-
Polyn::Errors::SchemaError
|
40
|
-
"
|
41
|
-
"been added to your `events` codebase and has been loaded "\
|
42
|
-
"into the schema store on your NATS server",
|
43
|
-
)
|
56
|
+
raise Polyn::Errors::SchemaError, "The Schema Store #{@store_name} has "\
|
57
|
+
"not been setup on your NATS server. Make sure you use Polyn CLI to create it"
|
44
58
|
end
|
45
59
|
|
46
|
-
def
|
47
|
-
|
60
|
+
def loop_store_keys
|
61
|
+
sub = @nats.jetstream.subscribe("#{@key_prefix}.>")
|
62
|
+
results = {}
|
63
|
+
|
64
|
+
loop do
|
65
|
+
msg = sub.next_msg
|
66
|
+
results[msg.subject.gsub("#{@key_prefix}.", "")] = msg.data unless msg.data.empty?
|
67
|
+
# A timeout is the only mechanism given to indicate there are no
|
68
|
+
# more messages
|
69
|
+
rescue NATS::IO::Timeout
|
70
|
+
break
|
71
|
+
end
|
72
|
+
|
73
|
+
sub.unsubscribe
|
74
|
+
results
|
48
75
|
end
|
49
76
|
end
|
50
77
|
end
|
@@ -22,50 +22,54 @@ module Polyn
|
|
22
22
|
##
|
23
23
|
# Handles serializing and deserializing data to and from JSON.
|
24
24
|
class Json
|
25
|
-
def
|
25
|
+
def initialize(schema_store)
|
26
|
+
@schema_store = schema_store
|
27
|
+
end
|
28
|
+
|
29
|
+
def serialize!(event)
|
26
30
|
validate_event_instance!(event)
|
27
31
|
event = event.to_h
|
28
|
-
validate!(
|
32
|
+
validate!(event)
|
29
33
|
JSON.generate(event)
|
30
34
|
end
|
31
35
|
|
32
|
-
def
|
33
|
-
data = deserialize(
|
36
|
+
def deserialize!(json)
|
37
|
+
data = deserialize(json)
|
34
38
|
raise data if data.is_a?(Polyn::Errors::Error)
|
35
39
|
|
36
40
|
data
|
37
41
|
end
|
38
42
|
|
39
|
-
def
|
43
|
+
def deserialize(json)
|
40
44
|
data = decode(json)
|
41
45
|
return data if data.is_a?(Polyn::Errors::Error)
|
42
46
|
|
43
|
-
error = validate(
|
47
|
+
error = validate(data)
|
44
48
|
return error if error.is_a?(Polyn::Errors::Error)
|
45
49
|
|
46
50
|
data = Polyn::Utils::Hash.deep_symbolize_keys(data)
|
47
51
|
Event.new(data)
|
48
52
|
end
|
49
53
|
|
50
|
-
def
|
54
|
+
def decode(json)
|
51
55
|
JSON.parse(json)
|
52
56
|
rescue JSON::ParserError
|
53
57
|
Polyn::Errors::ValidationError.new("Polyn was unable to decode the following message: \n#{json}")
|
54
58
|
end
|
55
59
|
|
56
|
-
def
|
57
|
-
result = validate(
|
60
|
+
def validate!(event)
|
61
|
+
result = validate(event)
|
58
62
|
raise result if result.is_a?(Polyn::Errors::Error)
|
59
63
|
end
|
60
64
|
|
61
|
-
def
|
65
|
+
def validate(event)
|
62
66
|
error = validate_cloud_event(event)
|
63
67
|
return error if error.is_a?(Polyn::Errors::Error)
|
64
68
|
|
65
|
-
validate_data(
|
69
|
+
validate_data(event)
|
66
70
|
end
|
67
71
|
|
68
|
-
def
|
72
|
+
def validate_event_instance!(event)
|
69
73
|
if event.instance_of?(Polyn::Event)
|
70
74
|
event
|
71
75
|
else
|
@@ -74,22 +78,22 @@ module Polyn
|
|
74
78
|
end
|
75
79
|
end
|
76
80
|
|
77
|
-
def
|
81
|
+
def validate_cloud_event(event)
|
78
82
|
cloud_event_schema = Polyn::CloudEvent.to_h
|
79
83
|
validate_schema(cloud_event_schema, event)
|
80
84
|
end
|
81
85
|
|
82
|
-
def
|
86
|
+
def validate_data(event)
|
83
87
|
type = get_event_type(event)
|
84
88
|
return type if type.is_a?(Polyn::Errors::Error)
|
85
89
|
|
86
|
-
schema = get_schema(
|
90
|
+
schema = get_schema(type)
|
87
91
|
return schema if schema.is_a?(Polyn::Errors::Error)
|
88
92
|
|
89
93
|
validate_schema(schema, event)
|
90
94
|
end
|
91
95
|
|
92
|
-
def
|
96
|
+
def validate_schema(schema, event)
|
93
97
|
schema = JSONSchemer.schema(schema)
|
94
98
|
errors = schema.validate(event).to_a
|
95
99
|
errors = format_schema_errors(errors)
|
@@ -101,7 +105,7 @@ module Polyn
|
|
101
105
|
errors
|
102
106
|
end
|
103
107
|
|
104
|
-
def
|
108
|
+
def get_event_type(event)
|
105
109
|
if event["type"]
|
106
110
|
Polyn::Naming.trim_domain_prefix(event["type"])
|
107
111
|
else
|
@@ -111,26 +115,22 @@ module Polyn
|
|
111
115
|
end
|
112
116
|
end
|
113
117
|
|
114
|
-
def
|
115
|
-
|
118
|
+
def get_schema(type)
|
119
|
+
@schema_store.get(type)
|
116
120
|
end
|
117
121
|
|
118
|
-
def
|
122
|
+
def format_schema_errors(errors)
|
119
123
|
errors.map do |error|
|
120
124
|
"Property: `#{error['data_pointer']}` - #{error['type']} - #{error['details']}"
|
121
125
|
end
|
122
126
|
end
|
123
127
|
|
124
|
-
def
|
128
|
+
def combined_error_message(event, errors)
|
125
129
|
[
|
126
130
|
"Polyn event #{event['id']} from #{event['source']} is not valid",
|
127
131
|
"Event data: #{event.inspect}",
|
128
132
|
].concat(errors).join("\n")
|
129
133
|
end
|
130
|
-
|
131
|
-
def self.store_name(**opts)
|
132
|
-
opts[:store_name] || Polyn::SchemaStore.store_name
|
133
|
-
end
|
134
134
|
end
|
135
135
|
end
|
136
136
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Polyn
|
4
|
+
class Testing
|
5
|
+
##
|
6
|
+
# Mock JetStream for applications to use in testing
|
7
|
+
class MockJetStream
|
8
|
+
def initialize(mock_nats)
|
9
|
+
@mock_nats = mock_nats
|
10
|
+
@real_nats = mock_nats.nats
|
11
|
+
end
|
12
|
+
|
13
|
+
def consumer_info(stream, consumer_name)
|
14
|
+
@real_nats.jetstream.consumer_info(stream, consumer_name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def find_stream_name_by_subject(subject)
|
18
|
+
@real_nats.jetstream.find_stream_name_by_subject(subject)
|
19
|
+
end
|
20
|
+
|
21
|
+
def pull_subscribe(subject, consumer_name)
|
22
|
+
Polyn::Testing::MockPullSubscription.new(
|
23
|
+
@mock_nats,
|
24
|
+
subject: subject,
|
25
|
+
consumer_name: consumer_name,
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Polyn
|
4
|
+
class Testing
|
5
|
+
class MockMsg
|
6
|
+
def initialize(opts = {})
|
7
|
+
@subject = opts[:subject]
|
8
|
+
@reply = opts[:reply]
|
9
|
+
@data = opts[:data]
|
10
|
+
@header = opts[:header]
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :subject, :reply, :data, :header
|
14
|
+
|
15
|
+
def ack(**params); end
|
16
|
+
|
17
|
+
def ack_sync(**params); end
|
18
|
+
|
19
|
+
def nak(**params); end
|
20
|
+
|
21
|
+
def term(**params); end
|
22
|
+
|
23
|
+
def in_progress(**params); end
|
24
|
+
|
25
|
+
def metadata; end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "nats/io/msg"
|
4
|
+
require "polyn/naming"
|
5
|
+
require "polyn/testing/mock_jetstream"
|
6
|
+
require "polyn/testing/mock_msg"
|
7
|
+
require "polyn/testing/mock_pull_subscription"
|
8
|
+
|
9
|
+
module Polyn
|
10
|
+
class Testing
|
11
|
+
##
|
12
|
+
# Mock Nats connection for applications to use in testing
|
13
|
+
class MockNats
|
14
|
+
def initialize(nats)
|
15
|
+
@nats = nats
|
16
|
+
@messages = []
|
17
|
+
@subscribers = []
|
18
|
+
@consumers = []
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :nats, :messages
|
22
|
+
|
23
|
+
def publish(subject, data, reply_to = nil, **opts)
|
24
|
+
msg = Polyn::Testing::MockMsg.new(subject: subject, data: data, reply: reply_to,
|
25
|
+
header: opts[:header])
|
26
|
+
send_to_subscribers(msg)
|
27
|
+
@messages << msg
|
28
|
+
update_consumers
|
29
|
+
end
|
30
|
+
|
31
|
+
def subscribe(subject, _opts = {}, &callback)
|
32
|
+
@subscribers << { subject: subject, callback: callback }
|
33
|
+
end
|
34
|
+
|
35
|
+
def jetstream
|
36
|
+
@jetstream ||= MockJetStream.new(self)
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_consumer(consumer)
|
40
|
+
@consumers << consumer
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def send_to_subscribers(msg)
|
46
|
+
@subscribers.each do |sub|
|
47
|
+
sub[:callback].call(msg) if Polyn::Naming.subject_matches?(msg.subject, sub[:subject])
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def update_consumers
|
52
|
+
@consumers.each(&:update_stream)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Polyn
|
4
|
+
class Testing
|
5
|
+
##
|
6
|
+
# Mock Pull Subscription for applications to use in testing
|
7
|
+
class MockPullSubscription
|
8
|
+
def initialize(mock_nats, **opts)
|
9
|
+
@mock_nats = mock_nats
|
10
|
+
@real_nats = mock_nats.nats
|
11
|
+
@subject = opts.fetch(:subject)
|
12
|
+
@consumer_name = opts.fetch(:consumer_name)
|
13
|
+
@stream = update_stream
|
14
|
+
@delivery_cursor = 0
|
15
|
+
@mock_nats.add_consumer(self)
|
16
|
+
end
|
17
|
+
|
18
|
+
def fetch(batch = 1, **_params)
|
19
|
+
start_pos = @delivery_cursor
|
20
|
+
end_pos = start_pos + batch - 1
|
21
|
+
update_cursor(end_pos)
|
22
|
+
@stream[start_pos..end_pos]
|
23
|
+
end
|
24
|
+
|
25
|
+
def update_stream
|
26
|
+
@stream = @mock_nats.messages.filter do |message|
|
27
|
+
Polyn::Naming.subject_matches?(message.subject, @subject)
|
28
|
+
end
|
29
|
+
@stream
|
30
|
+
end
|
31
|
+
|
32
|
+
def update_cursor(end_pos)
|
33
|
+
next_pos = end_pos + 1
|
34
|
+
|
35
|
+
@delivery_cursor = if @stream[next_pos]
|
36
|
+
next_pos
|
37
|
+
else
|
38
|
+
@stream.length
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "polyn/schema_store"
|
4
|
+
require "polyn/testing/mock_nats"
|
5
|
+
|
6
|
+
module Polyn
|
7
|
+
##
|
8
|
+
# Test helpers to help keep tests predictable
|
9
|
+
class Testing
|
10
|
+
##
|
11
|
+
# Use this in spec_helper.rb to include a shared_context that can be
|
12
|
+
# used with `include_context :polyn`
|
13
|
+
def self.setup(**_opts)
|
14
|
+
conn = NATS.connect
|
15
|
+
schema_store = Polyn::SchemaStore.new(conn)
|
16
|
+
|
17
|
+
RSpec.shared_context :polyn do
|
18
|
+
before(:each) do
|
19
|
+
# Have a global, shared schema store that only pulls the schemas
|
20
|
+
# once for the whole test suite. For testing an application the
|
21
|
+
# schemas are expected to be the same throughout the whole suite
|
22
|
+
Thread.current[:polyn_schema_store] = schema_store
|
23
|
+
end
|
24
|
+
|
25
|
+
after(:each) do
|
26
|
+
Thread.current[:polyn_schema_store] = nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/polyn/version.rb
CHANGED
data/lib/polyn.rb
CHANGED
@@ -28,6 +28,8 @@ require "polyn/cloud_event"
|
|
28
28
|
require "polyn/errors/errors"
|
29
29
|
require "polyn/event"
|
30
30
|
require "polyn/naming"
|
31
|
+
require "polyn/nats"
|
32
|
+
require "polyn/nats/msg"
|
31
33
|
require "polyn/pull_subscriber"
|
32
34
|
require "polyn/schema_store"
|
33
35
|
require "polyn/serializers/json"
|
@@ -38,79 +40,117 @@ require "polyn/version"
|
|
38
40
|
# Polyn is a Reactive service framework.
|
39
41
|
module Polyn
|
40
42
|
##
|
41
|
-
#
|
42
|
-
|
43
|
-
|
44
|
-
# @param type [String] The type of event
|
45
|
-
# @param data [any] The data to include in the event
|
46
|
-
# @option options [String] :source - information to specify the source of the event
|
47
|
-
# @option options [String] :triggered_by - The event that triggered this one.
|
48
|
-
# Will use information from the event to build up the `polyntrace` data
|
49
|
-
# @option options [String] :reply_to - Reply to a specific topic
|
50
|
-
# @option options [String] :header - Headers to include in the message
|
51
|
-
def self.publish(nats, type, data, **opts)
|
52
|
-
event = Event.new({
|
53
|
-
type: type,
|
54
|
-
source: opts[:source],
|
55
|
-
data: data,
|
56
|
-
triggered_by: opts[:triggered_by],
|
57
|
-
})
|
58
|
-
|
59
|
-
json = Polyn::Serializers::Json.serialize!(nats, event, **opts)
|
60
|
-
|
61
|
-
nats.publish(type, json, opts[:reply_to], header: opts[:header])
|
43
|
+
# Configuration information for Polyn
|
44
|
+
def self.configuration
|
45
|
+
@configuration ||= Configuration.new
|
62
46
|
end
|
63
47
|
|
64
|
-
##
|
65
|
-
#
|
66
|
-
|
67
|
-
|
68
|
-
# @param type [String] The type of event
|
69
|
-
# @option options [String] :queue - Queue group to add subscriber to
|
70
|
-
# @option options [String] :max - Max msgs before unsubscribing
|
71
|
-
# @option options [String] :pending_msgs_limit
|
72
|
-
# @option options [String] :pending_bytes_limit
|
73
|
-
def self.subscribe(nats, type, opts = {}, &callback)
|
74
|
-
nats.subscribe(type, opts) do |msg|
|
75
|
-
event = Polyn::Serializers::Json.deserialize!(nats, msg.data,
|
76
|
-
store_name: opts[:store_name])
|
77
|
-
msg.data = event
|
78
|
-
callback.call(msg)
|
79
|
-
end
|
48
|
+
##
|
49
|
+
# Configuration block to configure Polyn
|
50
|
+
def self.configure
|
51
|
+
yield(configuration)
|
80
52
|
end
|
81
53
|
|
82
54
|
##
|
83
|
-
#
|
55
|
+
# Connects Polyn to a NATS connection and loads all event schemas
|
84
56
|
#
|
85
|
-
# @param nats [
|
86
|
-
|
87
|
-
|
88
|
-
# is more than the `source_root`
|
89
|
-
def self.pull_subscribe(nats, type, **opts)
|
90
|
-
Polyn::PullSubscriber.new({ nats: nats, type: type, source: opts[:source] })
|
91
|
-
end
|
92
|
-
|
93
|
-
# nats-pure will create a consumer if the one you passed does not exist.
|
94
|
-
# Polyn wants to avoid this functionality and instead encourage
|
95
|
-
# consumer creation in the centralized `events` codebase so that
|
96
|
-
# it's documented, discoverable, and polyn-cli can manage it
|
97
|
-
def self.validate_consumer_exists!(nats, stream, consumer_name)
|
98
|
-
nats.jetstream.consumer_info(stream, consumer_name)
|
99
|
-
rescue NATS::JetStream::Error::NotFound
|
100
|
-
raise Polyn::Errors::ValidationError,
|
101
|
-
"Consumer #{consumer_name} does not exist. Use polyn-cli to create "\
|
102
|
-
"it before attempting to subscribe"
|
57
|
+
# @param nats [NATS::IO::Client] A nats connection instance
|
58
|
+
def self.connect(nats, **opts)
|
59
|
+
Conn.new(nats, **opts)
|
103
60
|
end
|
104
61
|
|
105
62
|
##
|
106
|
-
#
|
107
|
-
|
108
|
-
|
109
|
-
|
63
|
+
# A Polyn connection to NATS
|
64
|
+
class Conn
|
65
|
+
def initialize(nats, **opts)
|
66
|
+
@nats = nats_class.new(nats)
|
67
|
+
# Schema store nats has to be a real one, not a mock, because
|
68
|
+
# the only place to load the schemas is from a running nats-server
|
69
|
+
@schema_store = opts[:schema_store] || schema_store(nats, **opts)
|
70
|
+
@serializer = Polyn::Serializers::Json.new(@schema_store)
|
71
|
+
end
|
110
72
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
73
|
+
##
|
74
|
+
# Publishes a message on the Polyn network.
|
75
|
+
#
|
76
|
+
# @param type [String] The type of event
|
77
|
+
# @param data [any] The data to include in the event
|
78
|
+
# @option options [String] :source - information to specify the source of the event
|
79
|
+
# @option options [String] :triggered_by - The event that triggered this one.
|
80
|
+
# Will use information from the event to build up the `polyntrace` data
|
81
|
+
# @option options [String] :reply_to - Reply to a specific topic
|
82
|
+
# @option options [String] :header - Headers to include in the message
|
83
|
+
def publish(type, data, **opts)
|
84
|
+
event = Event.new({
|
85
|
+
type: type,
|
86
|
+
source: opts[:source],
|
87
|
+
data: data,
|
88
|
+
triggered_by: opts[:triggered_by],
|
89
|
+
})
|
90
|
+
|
91
|
+
# Ensure accidental message duplication doesn't happen
|
92
|
+
# https://docs.nats.io/using-nats/developer/develop_jetstream/model_deep_dive#message-deduplication
|
93
|
+
msg_id_header = { "Nats-Msg-Id" => event.id }
|
94
|
+
header = opts.fetch(:header, {})
|
95
|
+
header = msg_id_header.merge(header)
|
96
|
+
|
97
|
+
json = @serializer.serialize!(event)
|
98
|
+
|
99
|
+
@nats.publish(type, json, opts[:reply_to], header: header)
|
100
|
+
end
|
101
|
+
|
102
|
+
## Create subscription which is dispatched asynchronously
|
103
|
+
# and sends messages to a callback.
|
104
|
+
#
|
105
|
+
# @param type [String] The type of event
|
106
|
+
# @option options [String] :queue - Queue group to add subscriber to
|
107
|
+
# @option options [String] :max - Max msgs before unsubscribing
|
108
|
+
# @option options [String] :pending_msgs_limit
|
109
|
+
# @option options [String] :pending_bytes_limit
|
110
|
+
def subscribe(type, opts = {}, &callback)
|
111
|
+
@nats.subscribe(type, opts) do |msg|
|
112
|
+
event = @serializer.deserialize!(msg.data)
|
113
|
+
msg.data = event
|
114
|
+
callback.call(msg)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
##
|
119
|
+
# Subscribe to a pull consumer that already exists in the NATS server
|
120
|
+
#
|
121
|
+
# @param nats [Object] Connected NATS instance from `NATS.connect`
|
122
|
+
# @param type [String] The type of event
|
123
|
+
# @option options [String] :source - If the `source` portion of the consumer name
|
124
|
+
# is more than the `source_root`
|
125
|
+
def pull_subscribe(type, **opts)
|
126
|
+
Polyn::PullSubscriber.new({
|
127
|
+
nats: @nats,
|
128
|
+
type: type,
|
129
|
+
source: opts[:source],
|
130
|
+
serializer: @serializer,
|
131
|
+
})
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def schema_store(nats, **opts)
|
137
|
+
if Polyn.configuration.polyn_env == "test"
|
138
|
+
# For application tests reuse the same schema_store so we don't
|
139
|
+
# waste time fetching the schemas on every test
|
140
|
+
Thread.current[:polyn_schema_store]
|
141
|
+
else
|
142
|
+
Polyn::SchemaStore.new(nats, name: opts[:store_name])
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
##
|
147
|
+
# NATS connection class to use based on environment
|
148
|
+
def nats_class
|
149
|
+
if Polyn.configuration.polyn_env == "test"
|
150
|
+
Polyn::MockNats
|
151
|
+
else
|
152
|
+
Polyn::Nats
|
153
|
+
end
|
154
|
+
end
|
115
155
|
end
|
116
156
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: polyn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jarod
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2022-
|
12
|
+
date: 2022-09-07 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: json_schemer
|
@@ -88,9 +88,16 @@ files:
|
|
88
88
|
- lib/polyn/errors/validation_error.rb
|
89
89
|
- lib/polyn/event.rb
|
90
90
|
- lib/polyn/naming.rb
|
91
|
+
- lib/polyn/nats.rb
|
92
|
+
- lib/polyn/nats/msg.rb
|
91
93
|
- lib/polyn/pull_subscriber.rb
|
92
94
|
- lib/polyn/schema_store.rb
|
93
95
|
- lib/polyn/serializers/json.rb
|
96
|
+
- lib/polyn/testing.rb
|
97
|
+
- lib/polyn/testing/mock_jetstream.rb
|
98
|
+
- lib/polyn/testing/mock_msg.rb
|
99
|
+
- lib/polyn/testing/mock_nats.rb
|
100
|
+
- lib/polyn/testing/mock_pull_subscription.rb
|
94
101
|
- lib/polyn/utils/hash.rb
|
95
102
|
- lib/polyn/utils/string.rb
|
96
103
|
- lib/polyn/utils/utils.rb
|