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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 30b50a04cee7e5a540b48d8d80276ca87e8c3b247b70da41a9e00c6950a8c4d3
4
- data.tar.gz: '09c36399cec8f4e51188489d21aab4a49a41a8d420b273f4f5a9181b7c572b2d'
3
+ metadata.gz: 10ceff0b77d79960f425d6d7d6d1502095ed2b04533e9bd9fa5d37756f510e18
4
+ data.tar.gz: db2a410f4aa57312f72b7882c537a15d055fac0611f7eb7f2b31b3d5bf38a27e
5
5
  SHA512:
6
- metadata.gz: d631676305632f5379e6db7a8286b09b56b9764c118f2c957a06e13ea3bad8158365e164ec1e5a7f982d6578b7a32881fb972a5b4021c9422fb3a811f41031aa
7
- data.tar.gz: 6dde4a3a64edeaf92e06dc7f3a47c54d84f55309156fad1499ea8189639d9c0c6d72c6d14b8cc40b6cd2bef76b29d3fbfe43a889a42906cbca0d7ff1fe8210ad
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.1.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
- yard (0.9.26)
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
- Polyn.publish(nats, "user.created.v1", { name: "Mary" })
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
- Polyn.publish(nats, "user.created.v1", { name: "Mary" }, source: "new.users")
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
- Polyn.publish(nats, "user.created.v1", { name: "Mary" }, triggered_by: event)
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(nats, "user.created.v1")
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 = Polyn.subscribe(nats, "user.created.v1") { |msg| puts msg.data }
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 = Polyn.subscribe(nats, "user.created.v1") { |msg| puts msg.data }
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
@@ -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
- @store_name = store_name(fields)
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
- event = Polyn::Serializers::Json.deserialize(@nats, msg.data, store_name: @store_name)
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
@@ -4,47 +4,74 @@ module Polyn
4
4
  ##
5
5
  # Persisting and interacting with persisted schemas
6
6
  class SchemaStore
7
- STORE_NAME = "POLYN_SCHEMAS"
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 self.save(nats, type, schema, **opts)
21
+ def save(type, schema)
13
22
  json_schema?(schema)
14
- kv = nats.jetstream.key_value(store_name(**opts))
15
- kv.put(type, JSON.generate(schema))
23
+ schemas[type] = JSON.generate(schema)
16
24
  end
17
25
 
18
- def self.json_schema?(schema)
26
+ def json_schema?(schema)
19
27
  JSONSchemer.schema(schema)
20
28
  end
21
29
 
22
- def self.get!(nats, type, **opts)
23
- result = get(nats, type, **opts)
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 self.get(nats, type, **opts)
30
- kv = nats.jetstream.key_value(store_name(**opts))
31
- entry = kv.get(type)
32
- entry.value
33
- rescue NATS::KeyValue::BucketNotFoundError
34
- Polyn::Errors::SchemaError.new(
35
- "The Schema Store has not been setup on your NATS server. Make sure you use "\
36
- "the Polyn CLI to create it",
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.new(
40
- "Schema for #{type} does not exist. Make sure it's "\
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 self.store_name(**opts)
47
- opts.fetch(:name, STORE_NAME)
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 self.serialize!(nats, event, **opts)
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!(nats, event, **opts)
32
+ validate!(event)
29
33
  JSON.generate(event)
30
34
  end
31
35
 
32
- def self.deserialize!(nats, json, **opts)
33
- data = deserialize(nats, json, **opts)
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 self.deserialize(nats, json, **opts)
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(nats, data, **opts)
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 self.decode(json)
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 self.validate!(nats, event, **opts)
57
- result = validate(nats, event, **opts)
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 self.validate(nats, event, **opts)
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(nats, event, **opts)
69
+ validate_data(event)
66
70
  end
67
71
 
68
- def self.validate_event_instance!(event)
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 self.validate_cloud_event(event)
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 self.validate_data(nats, event, **opts)
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(nats, type, **opts)
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 self.validate_schema(schema, event)
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 self.get_event_type(event)
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 self.get_schema(nats, type, **opts)
115
- Polyn::SchemaStore.get(nats, type, name: store_name(**opts))
118
+ def get_schema(type)
119
+ @schema_store.get(type)
116
120
  end
117
121
 
118
- def self.format_schema_errors(errors)
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 self.combined_error_message(event, errors)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyn
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
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
- # Publishes a message on the Polyn network.
42
- #
43
- # @param nats [Object] Connected NATS instance from `NATS.connect`
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
- ## Create subscription which is dispatched asynchronously
65
- # and sends messages to a callback.
66
- #
67
- # @param nats [Object] Connected NATS instance from `NATS.connect`
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
- # Subscribe to a pull consumer that already exists in the NATS server
55
+ # Connects Polyn to a NATS connection and loads all event schemas
84
56
  #
85
- # @param nats [Object] Connected NATS instance from `NATS.connect`
86
- # @param type [String] The type of event
87
- # @option options [String] :source - If the `source` portion of the consumer name
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
- # Configuration information for Polyn
107
- def self.configuration
108
- @configuration ||= Configuration.new
109
- end
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
- # Configuration block to configure Polyn
113
- def self.configure
114
- yield(configuration)
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.1.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-08-18 00:00:00.000000000 Z
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