activecypher 0.13.0 → 0.14.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 96c24fcfa519e4e44a2e8c8d1e84ae0b975d0d73d25492b025f4968e148a2090
4
- data.tar.gz: f9ef843ec7859048e7284731122be13ec519f9d44066471db70db1d4d326198f
3
+ metadata.gz: bfba386d94660c96cf6078ac0a7d370d2dd5670e9316a3de2ee917f409f227ab
4
+ data.tar.gz: e2e70d29dc99279b51f810363d50749e470556481e80c8c8276fc80ba612b7d4
5
5
  SHA512:
6
- metadata.gz: 0afa400aeab7bfc05a7842cf4361f064b052ec52c8d2fc402da767da882277e8ad1b2c14a2b3c6396c3d34cc436caa45cb43978727d4cd02da80e589af197b10
7
- data.tar.gz: e76af4465bdfb80f6b388137e849b2b2705e1f9fd8ae5afd81369f19d40c144819cc5eba6f106677a37932b7a0bbab864951fef2e5fb2450aca982e81971065b
6
+ metadata.gz: 1fbe83583d2cdb4a7ea898e8ce5876cfd6907af332ffd7b77643473026ac5fd0d3714bcf037876d2c8ebd8722594a9e5635ae0b3c816c4a98ed9f49dad99e523
7
+ data.tar.gz: bc13b5392ce91d0247bd4166310e8967676763b9d11f3156206f7a8e3b004f3158b5341dfbadfec82677fc887ef6aedfc8fe51081ea722d847cce60eb155f1d0
@@ -88,10 +88,18 @@ module ActiveCypher
88
88
  run_response = connection.read_message
89
89
  unless run_response.is_a?(Bolt::Messaging::Success)
90
90
  # Read any remaining messages to clear connection state
91
- connection.read_message rescue nil
91
+ begin
92
+ connection.read_message
93
+ rescue StandardError
94
+ nil
95
+ end
92
96
  # Send RESET to clear connection state
93
97
  connection.write_message(Bolt::Messaging::Reset.new)
94
- connection.read_message rescue nil
98
+ begin
99
+ connection.read_message
100
+ rescue StandardError
101
+ nil
102
+ end
95
103
  raise QueryError, "DDL failed for: #{cypher.inspect}\nError: #{run_response.fields.first}"
96
104
  end
97
105
 
@@ -100,22 +108,71 @@ module ActiveCypher
100
108
  end
101
109
  end
102
110
 
103
- # Override run to execute queries without explicit transactions
104
- # Memgraph autocommits each query, so we send RUN + PULL directly
111
+ # Override run to execute queries using auto-commit mode.
112
+ # Memgraph auto-commits each query, so we send RUN + PULL directly
113
+ # without BEGIN/COMMIT wrapper. This avoids transaction state issues.
105
114
  def run(cypher, params = {}, context: 'Query', db: nil, access_mode: :write)
106
115
  connect
107
116
  logger.debug { "[#{context}] #{cypher} #{params.inspect}" }
108
117
 
109
118
  instrument_query(cypher, params, context: context, metadata: { db: db, access_mode: access_mode }) do
110
- session = Bolt::Session.new(connection)
119
+ run_auto_commit(cypher, prepare_params(params))
120
+ end
121
+ end
111
122
 
112
- rows = session.run_transaction(access_mode, db: db) do |tx|
113
- result = tx.run(cypher, prepare_params(params))
114
- result.respond_to?(:to_a) ? result.to_a : result
115
- end
123
+ # Execute a query in auto-commit mode (no explicit transaction).
124
+ # Sends RUN + PULL directly to the connection.
125
+ #
126
+ # @param cypher [String] The Cypher query
127
+ # @param params [Hash] Query parameters
128
+ # @return [Array<Hash>] The result rows
129
+ def run_auto_commit(cypher, params = {})
130
+ Sync do
131
+ # Send RUN message
132
+ run_meta = {}
133
+ connection.write_message(Bolt::Messaging::Run.new(cypher, params, run_meta))
116
134
 
117
- session.close
118
- rows
135
+ # Read RUN response
136
+ run_response = connection.read_message
137
+
138
+ case run_response
139
+ when Bolt::Messaging::Success
140
+ # Send PULL to get results
141
+ connection.write_message(Bolt::Messaging::Pull.new({ n: -1 }))
142
+
143
+ # Collect records
144
+ rows = []
145
+ fields = run_response.metadata['fields'] || []
146
+
147
+ loop do
148
+ msg = connection.read_message
149
+ case msg
150
+ when Bolt::Messaging::Record
151
+ # Convert record values to hash with field names
152
+ row = fields.zip(msg.values).to_h
153
+ rows << row
154
+ when Bolt::Messaging::Success
155
+ # End of results
156
+ break
157
+ when Bolt::Messaging::Failure
158
+ code = msg.metadata['code']
159
+ message = msg.metadata['message']
160
+ connection.reset!
161
+ raise QueryError, "Query failed: #{code} - #{message}"
162
+ else
163
+ raise ProtocolError, "Unexpected response during PULL: #{msg.class}"
164
+ end
165
+ end
166
+
167
+ rows
168
+ when Bolt::Messaging::Failure
169
+ code = run_response.metadata['code']
170
+ message = run_response.metadata['message']
171
+ connection.reset!
172
+ raise QueryError, "Query failed: #{code} - #{message}"
173
+ else
174
+ raise ProtocolError, "Unexpected response to RUN: #{run_response.class}"
175
+ end
119
176
  end
120
177
  end
121
178
 
@@ -213,8 +270,13 @@ module ActiveCypher
213
270
 
214
271
  def protocol_handler_class = ProtocolHandler
215
272
 
273
+ # Memgraph 3.7+ is expected. Earlier versions are untested and may not work.
274
+ # See: https://memgraph.com/docs for version-specific features.
275
+ MINIMUM_VERSION = '3.7'
276
+
216
277
  def validate_connection
217
- raise ActiveCypher::ConnectionError, "Server at #{config[:uri]} is not Memgraph" unless connection.server_agent.to_s.include?('Memgraph')
278
+ agent = connection.server_agent.to_s
279
+ raise ActiveCypher::ConnectionError, "Server at #{config[:uri]} is not Memgraph" unless agent.include?('Memgraph')
218
280
 
219
281
  true
220
282
  end
@@ -28,10 +28,26 @@ module ActiveCypher
28
28
 
29
29
  # DSL ---------------------------------------------------------------
30
30
 
31
- def create_node_index(label, *props, unique: false, if_not_exists: true, name: nil)
31
+ # Create a node property index.
32
+ # @param label [Symbol, String] Node label
33
+ # @param props [Array<Symbol>] Properties to index
34
+ # @param unique [Boolean] Create unique index (Neo4j only)
35
+ # @param if_not_exists [Boolean] Add IF NOT EXISTS clause (Neo4j only)
36
+ # @param name [String] Index name (Neo4j only)
37
+ # @param composite [Boolean] Create composite index (Memgraph 3.2+). Default true for multiple props.
38
+ def create_node_index(label, *props, unique: false, if_not_exists: true, name: nil, composite: nil)
39
+ # Default composite to true when multiple properties provided
40
+ composite = props.size > 1 if composite.nil?
41
+
32
42
  cypher = if connection.vendor == :memgraph
33
- # Memgraph syntax: CREATE INDEX ON :Label(prop)
34
- props.map { |p| "CREATE INDEX ON :#{label}(#{p})" }
43
+ if composite && props.size > 1
44
+ # Memgraph 3.2+ composite index: CREATE INDEX ON :Label(prop1, prop2)
45
+ props_list = props.join(', ')
46
+ ["CREATE INDEX ON :#{label}(#{props_list})"]
47
+ else
48
+ # Memgraph single property indexes
49
+ props.map { |p| "CREATE INDEX ON :#{label}(#{p})" }
50
+ end
35
51
  else
36
52
  # Neo4j syntax
37
53
  props_clause = props.map { |p| "n.#{p}" }.join(', ')
@@ -46,10 +62,23 @@ module ActiveCypher
46
62
  operations.concat(Array(cypher))
47
63
  end
48
64
 
49
- def create_rel_index(rel_type, *props, if_not_exists: true, name: nil)
65
+ # Create a relationship property index.
66
+ # @param rel_type [Symbol, String] Relationship type
67
+ # @param props [Array<Symbol>] Properties to index
68
+ # @param if_not_exists [Boolean] Add IF NOT EXISTS clause (Neo4j only)
69
+ # @param name [String] Index name (Neo4j only)
70
+ # @param composite [Boolean] Create composite index (Memgraph 3.2+). Default true for multiple props.
71
+ def create_rel_index(rel_type, *props, if_not_exists: true, name: nil, composite: nil)
72
+ composite = props.size > 1 if composite.nil?
73
+
50
74
  cypher = if connection.vendor == :memgraph
51
- # Memgraph syntax: CREATE EDGE INDEX ON :REL_TYPE(prop)
52
- props.map { |p| "CREATE EDGE INDEX ON :#{rel_type}(#{p})" }
75
+ if composite && props.size > 1
76
+ # Memgraph 3.2+ composite edge index
77
+ props_list = props.join(', ')
78
+ ["CREATE EDGE INDEX ON :#{rel_type}(#{props_list})"]
79
+ else
80
+ props.map { |p| "CREATE EDGE INDEX ON :#{rel_type}(#{p})" }
81
+ end
53
82
  else
54
83
  # Neo4j syntax
55
84
  props_clause = props.map { |p| "r.#{p}" }.join(', ')
@@ -99,6 +128,94 @@ module ActiveCypher
99
128
  operations.concat(Array(cypher))
100
129
  end
101
130
 
131
+ # Create a vector index (Memgraph 3.4+, Neo4j 5.0+).
132
+ # @param name [String] Index name
133
+ # @param label [Symbol, String] Node label
134
+ # @param property [Symbol] Property containing vector embeddings
135
+ # @param dimension [Integer] Vector dimension (required)
136
+ # @param metric [Symbol] Distance metric: :cosine, :euclidean, :dot_product (default: :cosine)
137
+ # @param quantization [Symbol] Quantization type for memory reduction (Memgraph 3.4+): :scalar, nil
138
+ def create_vector_index(name, label, property, dimension:, metric: :cosine, quantization: nil)
139
+ cypher = if connection.vendor == :memgraph
140
+ config = { dimension: dimension, metric: metric.to_s }
141
+ config[:scalar_kind] = 'f32' if quantization == :scalar
142
+ config_str = config.map { |k, v| "#{k}: #{v.is_a?(String) ? "'#{v}'" : v}" }.join(', ')
143
+ "CREATE VECTOR INDEX #{name} ON :#{label}(#{property}) WITH CONFIG { #{config_str} }"
144
+ else
145
+ # Neo4j syntax
146
+ options = { indexConfig: { 'vector.dimensions' => dimension, 'vector.similarity_function' => metric.to_s.upcase } }
147
+ opts_str = options.to_json.gsub('"', "'")
148
+ "CREATE VECTOR INDEX #{name} IF NOT EXISTS FOR (n:#{label}) ON (n.#{property}) OPTIONS #{opts_str}"
149
+ end
150
+ operations << cypher
151
+ end
152
+
153
+ # Create a vector index on relationships (Memgraph 3.4+, Neo4j 2025+).
154
+ # @param name [String] Index name
155
+ # @param rel_type [Symbol, String] Relationship type
156
+ # @param property [Symbol] Property containing vector embeddings
157
+ # @param dimension [Integer] Vector dimension (required)
158
+ # @param metric [Symbol] Distance metric: :cosine, :euclidean, :dot_product (default: :cosine)
159
+ def create_vector_rel_index(name, rel_type, property, dimension:, metric: :cosine)
160
+ cypher = if connection.vendor == :memgraph
161
+ config_str = "dimension: #{dimension}, metric: '#{metric}'"
162
+ "CREATE VECTOR EDGE INDEX #{name} ON :#{rel_type}(#{property}) WITH CONFIG { #{config_str} }"
163
+ else
164
+ # Neo4j 2025+ syntax
165
+ "CREATE VECTOR INDEX #{name} IF NOT EXISTS FOR ()-[r:#{rel_type}]-() ON (r.#{property}) " \
166
+ "OPTIONS { indexConfig: { `vector.dimensions`: #{dimension}, `vector.similarity_function`: '#{metric}' } }"
167
+ end
168
+ operations << cypher
169
+ end
170
+
171
+ # Alias for backwards compatibility
172
+ alias create_vector_edge_index create_vector_rel_index
173
+
174
+ # Create a text index on edges (Memgraph 3.6+ only).
175
+ # Neo4j fulltext indexes on relationships use different syntax via create_fulltext_rel_index.
176
+ # @param name [String] Index name
177
+ # @param rel_type [Symbol, String] Relationship type
178
+ # @param props [Array<Symbol>] Properties to index
179
+ def create_text_edge_index(name, rel_type, *props)
180
+ raise NotImplementedError, 'Text edge indexes only supported on Memgraph 3.6+' unless connection.vendor == :memgraph
181
+
182
+ props.each do |p|
183
+ index_name = props.size > 1 ? "#{name}_#{p}" : name.to_s
184
+ operations << "CREATE TEXT EDGE INDEX #{index_name} ON :#{rel_type}(#{p})"
185
+ end
186
+ end
187
+
188
+ # Create a fulltext index on relationships (Neo4j only).
189
+ # @param name [String] Index name
190
+ # @param rel_type [Symbol, String] Relationship type
191
+ # @param props [Array<Symbol>] Properties to index
192
+ # @param if_not_exists [Boolean] Add IF NOT EXISTS clause
193
+ def create_fulltext_rel_index(name, rel_type, *props, if_not_exists: true)
194
+ raise NotImplementedError, 'Fulltext relationship indexes only supported on Neo4j' unless connection.vendor == :neo4j
195
+
196
+ props_clause = props.map { |p| "r.#{p}" }.join(', ')
197
+ c = +"CREATE FULLTEXT INDEX #{name}"
198
+ c << ' IF NOT EXISTS' if if_not_exists
199
+ c << " FOR ()-[r:#{rel_type}]-() ON EACH [#{props_clause}]"
200
+ operations << c
201
+ end
202
+
203
+ # Drop all indexes (Memgraph 3.6+ only).
204
+ # Neo4j requires dropping indexes individually.
205
+ def drop_all_indexes
206
+ raise NotImplementedError, 'drop_all_indexes only supported on Memgraph 3.6+' unless connection.vendor == :memgraph
207
+
208
+ operations << 'DROP ALL INDEXES'
209
+ end
210
+
211
+ # Drop all constraints (Memgraph 3.6+ only).
212
+ # Neo4j requires dropping constraints individually.
213
+ def drop_all_constraints
214
+ raise NotImplementedError, 'drop_all_constraints only supported on Memgraph 3.6+' unless connection.vendor == :memgraph
215
+
216
+ operations << 'DROP ALL CONSTRAINTS'
217
+ end
218
+
102
219
  def execute(cypher_string)
103
220
  operations << cypher_string.strip
104
221
  end
@@ -59,26 +59,26 @@ module ActiveCypher
59
59
  # --------------------------------------------------------------
60
60
  # Connection fallback
61
61
  # --------------------------------------------------------------
62
- # Relationship classes usually share the same Bolt pool as the
63
- # node they originate from; delegate there unless the relationship
64
- # class was given its own pool explicitly.
65
- #
66
- # WorksAtRelationship.connection # -> PersonNode.connection
67
- #
68
- def self.connection
69
- # If this is a concrete relationship class with from_class defined,
70
- # prefer delegating to that node's connection (so role/shard routing is respected).
71
- if !abstract_class? && (fc = from_class_name)
72
- klass = fc.constantize
73
- role = ActiveCypher::RuntimeRegistry.current_role
74
- shard = ActiveCypher::RuntimeRegistry.current_shard
75
-
76
- return klass.connected_to(role: role, shard: shard) do
77
- klass.connection
78
- end
62
+ # Relationship classes usually share the same Bolt pool as the
63
+ # node they originate from; delegate there unless the relationship
64
+ # class was given its own pool explicitly.
65
+ #
66
+ # WorksAtRelationship.connection # -> PersonNode.connection
67
+ #
68
+ def self.connection
69
+ # If this is a concrete relationship class with from_class defined,
70
+ # prefer delegating to that node's connection (so role/shard routing is respected).
71
+ if !abstract_class? && (fc = from_class_name)
72
+ klass = fc.constantize
73
+ role = ActiveCypher::RuntimeRegistry.current_role
74
+ shard = ActiveCypher::RuntimeRegistry.current_shard
75
+
76
+ return klass.connected_to(role: role, shard: shard) do
77
+ klass.connection
79
78
  end
79
+ end
80
80
 
81
- # Otherwise, fall back to node_base_class if present (even if abstract)
81
+ # Otherwise, fall back to node_base_class if present (even if abstract)
82
82
  if (klass = node_base_class)
83
83
  return klass.connection
84
84
  end
@@ -313,12 +313,10 @@ module ActiveCypher
313
313
  end
314
314
  rescue ActiveCypher::RecordNotSaved, RuntimeError => e
315
315
  # Only catch specific validation errors, let other errors propagate
316
- if e.message.include?('must be persisted') || e.message.include?('creation returned no id')
317
- log_error "Failed to save #{self.class}: #{e.message}"
318
- false
319
- else
320
- raise
321
- end
316
+ raise unless e.message.include?('must be persisted') || e.message.include?('creation returned no id')
317
+
318
+ log_error "Failed to save #{self.class}: #{e.message}"
319
+ false
322
320
  end
323
321
 
324
322
  # Bang version of save - raises exception if save fails
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveCypher
4
- VERSION = '0.13.0'
4
+ VERSION = '0.14.1'
5
5
 
6
6
  def self.gem_version
7
7
  Gem::Version.new VERSION
@@ -35,5 +35,38 @@ module Cyrel
35
35
  "EXISTS(#{rendered_pattern})"
36
36
  end
37
37
  end
38
+
39
+ # Represents an EXISTS { MATCH ... WHERE ... } subquery predicate (Memgraph 3.5+).
40
+ # Allows full subquery syntax inside EXISTS block.
41
+ #
42
+ # @example
43
+ # Cyrel.exists_block { match(Cyrel.node(:a) > Cyrel.rel(:r) > Cyrel.node(:b)); where(Cyrel.prop(:b, :active) == true) }
44
+ # # => EXISTS { MATCH (a)-[r]->(b) WHERE b.active = $p1 }
45
+ class ExistsBlock < Base
46
+ attr_reader :subquery
47
+
48
+ # @param subquery [Cyrel::Query] A query object representing the subquery.
49
+ def initialize(subquery)
50
+ raise ArgumentError, 'ExistsBlock requires a Cyrel::Query' unless subquery.is_a?(Cyrel::Query)
51
+
52
+ @subquery = subquery
53
+ end
54
+
55
+ # Renders the EXISTS { ... } expression.
56
+ # @param query [Cyrel::Query] The parent query for parameter merging.
57
+ # @return [String] The Cypher string fragment.
58
+ def render(query)
59
+ inner_cypher, inner_params = @subquery.to_cypher
60
+
61
+ # Merge subquery parameters into the parent query
62
+ # The inner query uses its own parameter keys, we need to register values
63
+ # which will get new keys in the parent query
64
+ inner_params.each_value do |value|
65
+ query.register_parameter(value)
66
+ end
67
+
68
+ "EXISTS { #{inner_cypher} }"
69
+ end
70
+ end
38
71
  end
39
72
  end
@@ -12,14 +12,16 @@ module Cyrel
12
12
 
13
13
  attribute :alias_name, Cyrel::Types::SymbolType.new
14
14
  attribute :labels, array: :string, default: []
15
+ attribute :or_labels, array: :string, default: [] # Memgraph 3.2+: (n:Label1|Label2)
15
16
  attribute :properties, Cyrel::Types::HashType.new, default: -> { {} }
16
17
 
17
18
  validates :alias_name, presence: true
18
19
 
19
- def initialize(alias_name, labels: nil, properties: {}, **kw)
20
+ def initialize(alias_name, labels: nil, or_labels: nil, properties: {}, **kw)
20
21
  super(
21
22
  { alias_name: alias_name,
22
23
  labels: Array(labels).compact.flatten,
24
+ or_labels: Array(or_labels).compact.flatten,
23
25
  properties: properties }.merge(kw)
24
26
  )
25
27
  end
@@ -38,7 +40,14 @@ module Cyrel
38
40
 
39
41
  def render(query)
40
42
  base = +"(#{alias_name}"
41
- base << ':' << labels.join(':') unless labels.empty?
43
+
44
+ # OR labels take precedence (Memgraph 3.2+: n:Label1|Label2)
45
+ if or_labels.any?
46
+ base << ':' << or_labels.join('|')
47
+ elsif labels.any?
48
+ base << ':' << labels.join(':')
49
+ end
50
+
42
51
  unless properties.empty?
43
52
  params = properties.with_indifferent_access
44
53
  formatted = params.map do |k, v|
@@ -60,6 +69,7 @@ module Cyrel
60
69
  def freeze
61
70
  super
62
71
  labels.freeze
72
+ or_labels.freeze
63
73
  properties.freeze
64
74
  end
65
75
 
data/lib/cyrel.rb CHANGED
@@ -11,8 +11,9 @@ module Cyrel
11
11
 
12
12
  # Cyrel DSL helper: alias for node creation.
13
13
  # Example: Cyrel.n(:person, :Person, name: 'Alice')
14
- def n(alias_name = nil, *labels, **properties)
15
- Pattern::Node.new(alias_name, labels: labels, properties: properties)
14
+ # Example: Cyrel.n(:n, or_labels: [:Person, :Organization]) # Memgraph 3.2+
15
+ def n(alias_name = nil, *labels, or_labels: nil, **properties)
16
+ Pattern::Node.new(alias_name, labels: labels, or_labels: or_labels, properties: properties)
16
17
  end
17
18
 
18
19
  # Cyrel DSL helper: creates a CALL clause for a procedure.
@@ -29,8 +30,9 @@ module Cyrel
29
30
 
30
31
  # Cyrel DSL helper: creates a node pattern.
31
32
  # Example: Cyrel.node(:n, :Person, name: 'Alice')
32
- def node(alias_name = nil, *labels, **properties)
33
- Pattern::Node.new(alias_name, labels: labels, properties: properties)
33
+ # Example: Cyrel.node(:n, or_labels: [:Person, :Organization]) # Memgraph 3.2+
34
+ def node(alias_name = nil, *labels, or_labels: nil, **properties)
35
+ Pattern::Node.new(alias_name, labels: labels, or_labels: or_labels, properties: properties)
34
36
  end
35
37
 
36
38
  # Cyrel DSL helper: creates a relationship pattern.
@@ -57,14 +59,14 @@ module Cyrel
57
59
  @pending_direction = nil
58
60
  end
59
61
 
60
- def node(alias_name = nil, *labels, **properties)
62
+ def node(alias_name = nil, *labels, or_labels: nil, **properties)
61
63
  # If there's a pending direction, we need to add a relationship first
62
64
  if @pending_direction && @elements.any? && @elements.last.is_a?(Cyrel::Pattern::Node)
63
65
  @elements << Cyrel::Pattern::Relationship.new(types: [], direction: @pending_direction)
64
66
  @pending_direction = nil
65
67
  end
66
68
 
67
- n = Cyrel::Pattern::Node.new(alias_name, labels: labels, properties: properties)
69
+ n = Cyrel::Pattern::Node.new(alias_name, labels: labels, or_labels: or_labels, properties: properties)
68
70
  @elements << n
69
71
  self
70
72
  end
@@ -250,6 +252,16 @@ module Cyrel
250
252
  Expression.exists(pattern)
251
253
  end
252
254
 
255
+ # Cyrel DSL helper: creates an EXISTS block with full subquery (Memgraph 3.5+).
256
+ # Example:
257
+ # Cyrel.exists_block { match(Cyrel.node(:a) > Cyrel.rel(:r) > Cyrel.node(:b, :Admin)) }
258
+ # # => EXISTS { MATCH (a)-[r]->(b:Admin) }
259
+ def exists_block(&block)
260
+ subquery = Query.new
261
+ subquery.instance_eval(&block)
262
+ Expression::ExistsBlock.new(subquery)
263
+ end
264
+
253
265
  # Cyrel DSL helper: creates a Logical NOT expression.
254
266
  # Example: Cyrel.not(expression)
255
267
  def not(expression)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activecypher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.14.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -43,14 +43,14 @@ dependencies:
43
43
  requirements:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: 0.11.0
46
+ version: 0.11.1
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: 0.11.0
53
+ version: 0.11.1
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: io-endpoint
56
56
  requirement: !ruby/object:Gem::Requirement