nanoc-rust 0.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8db56be993dae34ec1512f2cd9af7467c7600529
4
+ data.tar.gz: a0514e0c847376638155b4f8e6e46038ad3902fa
5
+ SHA512:
6
+ metadata.gz: cd68879cd68f971b3d6295288f782d0a3392c716797df889159ccc0187ef24efa41effa961cbff1a172969e082dd00d439213dbc24a9a7ac09174c16b6d5e727
7
+ data.tar.gz: 756b079f316ef77d1b8c4c0abf85ff510f0a649cf7a15feb4947c93b8ebffb08b13fa789456d92096a7867017099bb44517c01696980bf83846ca3c7f35a6d6a
@@ -0,0 +1,3 @@
1
+ dist/
2
+ target/
3
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ -r ./spec/spec_helper.rb
2
+ --format Fuubar
3
+ --color
@@ -0,0 +1,174 @@
1
+ # ----- CONFIGURED -----
2
+
3
+ AllCops:
4
+ TargetRubyVersion: 2.3
5
+ DisplayCopNames: true
6
+
7
+ # We use filenames such as “create-site.rb” that translate to method names.
8
+ FileName:
9
+ Exclude:
10
+ - 'lib/nanoc/cli/commands/*.rb'
11
+ - 'Appraisals'
12
+
13
+ # A common pattern in tests is to define anonymous classes in which methods are defined, which trips
14
+ # up Rubocop’s nested method definition cop.
15
+ Lint/NestedMethodDefinition:
16
+ Exclude:
17
+ - 'test/**/*.rb'
18
+ - 'spec/**/*.rb'
19
+
20
+ # This is used in tests, to verify the effect of state-changing functions.
21
+ Style/GlobalVars:
22
+ Exclude:
23
+ - 'test/**/*.rb'
24
+
25
+ Style/TrailingCommaInArguments:
26
+ EnforcedStyleForMultiline: comma
27
+
28
+ Style/TrailingCommaInLiteral:
29
+ EnforcedStyleForMultiline: comma
30
+
31
+ # `rescue nil` is useful in specs where the exception is not important, but
32
+ # the size effects are.
33
+ Style/RescueModifier:
34
+ Exclude:
35
+ - 'spec/**/*.rb'
36
+
37
+ Layout/IndentArray:
38
+ EnforcedStyle: consistent
39
+
40
+ Lint/DuplicateMethods:
41
+ Exclude:
42
+ - 'test/data_sources/test_filesystem.rb'
43
+ - 'spec/spec_helper.rb'
44
+
45
+ # This needs to be fixed in Ruby 2.4.
46
+ Lint/UnifiedInteger:
47
+ Enabled: false
48
+
49
+ Layout/IndentHeredoc:
50
+ EnforcedStyle: squiggly
51
+
52
+ # This breaks RSpec on occasion, e.g. `expect { subject }.not_to change { foo }`,
53
+ # and generally does not provide useful warnings
54
+ Lint/AmbiguousBlockAssociation:
55
+ Enabled: false
56
+
57
+ # ----- TO ENABLE LATER -----
58
+
59
+ # Valid cops, but fixing the offenses they report is non-trivial.
60
+
61
+ RegexpLiteral:
62
+ Enabled: false
63
+
64
+ ClassAndModuleChildren:
65
+ Enabled: false
66
+
67
+ Style/EmptyElse:
68
+ Enabled: false
69
+
70
+ Style/Next:
71
+ Enabled: false
72
+
73
+
74
+
75
+ # ----- DISABLED (hard) -----
76
+
77
+ # Rubocop trips up on this.
78
+ Layout/LeadingCommentSpace:
79
+ Enabled: false
80
+
81
+
82
+
83
+ # ----- DISABLED (security) -----
84
+
85
+ # Nanoc runs offline in a trusted environment, and these security checks are false positives.
86
+
87
+ Security/YAMLLoad:
88
+ Enabled: false
89
+
90
+ Security/MarshalLoad:
91
+ Enabled: false
92
+
93
+ Security/Eval:
94
+ Exclude:
95
+ - 'test/**/*.rb'
96
+ - 'spec/**/*.rb'
97
+ - 'lib/nanoc/base/entities/code_snippet.rb'
98
+ - 'lib/nanoc/filters/erubi.rb'
99
+
100
+
101
+ # ----- DISABLED (metrics) -----
102
+
103
+ # Cops for metrics are disabled because they should not cause tests to fail.
104
+
105
+ Metrics/AbcSize:
106
+ Enabled: false
107
+
108
+ Metrics/BlockLength:
109
+ Enabled: false
110
+
111
+ Metrics/BlockNesting:
112
+ Enabled: false
113
+
114
+ Metrics/ClassLength:
115
+ Enabled: false
116
+
117
+ Metrics/CyclomaticComplexity:
118
+ Enabled: false
119
+
120
+ Metrics/LineLength:
121
+ Enabled: false
122
+
123
+ Metrics/MethodLength:
124
+ Enabled: false
125
+
126
+ Metrics/ModuleLength:
127
+ Enabled: false
128
+
129
+ Metrics/ParameterLists:
130
+ Enabled: false
131
+
132
+ Metrics/PerceivedComplexity:
133
+ Enabled: false
134
+
135
+
136
+
137
+ # ----- DISABLED (opinionated) -----
138
+
139
+ # We should embrace UTF-8, not avoid it. Since the Encoding cop is enabled,
140
+ # there’s no point in enforcing ASCII comments.
141
+ AsciiComments:
142
+ Enabled: false
143
+
144
+ # It does not make sense to enforce everything to have documentation.
145
+ Documentation:
146
+ Enabled: false
147
+
148
+ # Nanoc suppresses exceptions for valid reasons in a few cases.
149
+ HandleExceptions:
150
+ Enabled: false
151
+
152
+ # if/unless at the end of the line makes it too easy to oversee.
153
+ IfUnlessModifier:
154
+ Enabled: false
155
+
156
+ # Personal preference is to have decent constructors for exceptions rather than
157
+ # just a class and a message.
158
+ RaiseArgs:
159
+ Enabled: false
160
+
161
+ # Personal preference is to use `raise` to signal exceptions (normal control
162
+ # flow should not use exceptions anyway).
163
+ SignalException:
164
+ Enabled: false
165
+
166
+ # Some methods that appear to be accessors (return a single value or set a
167
+ # single value) should still not be considered to be accessors. This is a purely
168
+ # semantic difference.
169
+ TrivialAccessors:
170
+ Enabled: false
171
+
172
+ # This does not always semantically make sense.
173
+ GuardClause:
174
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ group :devel do
8
+ gem 'fuubar'
9
+ gem 'rake'
10
+ gem 'rspec'
11
+ gem 'rspec-its', '~> 1.2'
12
+ gem 'rspec-mocks'
13
+ gem 'rubocop', '~> 0.49'
14
+ end
@@ -0,0 +1,63 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ nanoc-rust (0.1)
5
+ ffi (~> 1.9)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ ast (2.3.0)
11
+ diff-lcs (1.3)
12
+ ffi (1.9.18)
13
+ fuubar (2.2.0)
14
+ rspec-core (~> 3.0)
15
+ ruby-progressbar (~> 1.4)
16
+ parallel (1.11.2)
17
+ parser (2.4.0.0)
18
+ ast (~> 2.2)
19
+ powerpack (0.1.1)
20
+ rainbow (2.2.2)
21
+ rake
22
+ rake (12.0.0)
23
+ rspec (3.6.0)
24
+ rspec-core (~> 3.6.0)
25
+ rspec-expectations (~> 3.6.0)
26
+ rspec-mocks (~> 3.6.0)
27
+ rspec-core (3.6.0)
28
+ rspec-support (~> 3.6.0)
29
+ rspec-expectations (3.6.0)
30
+ diff-lcs (>= 1.2.0, < 2.0)
31
+ rspec-support (~> 3.6.0)
32
+ rspec-its (1.2.0)
33
+ rspec-core (>= 3.0.0)
34
+ rspec-expectations (>= 3.0.0)
35
+ rspec-mocks (3.6.0)
36
+ diff-lcs (>= 1.2.0, < 2.0)
37
+ rspec-support (~> 3.6.0)
38
+ rspec-support (3.6.0)
39
+ rubocop (0.49.1)
40
+ parallel (~> 1.10)
41
+ parser (>= 2.3.3.1, < 3.0)
42
+ powerpack (~> 0.1)
43
+ rainbow (>= 1.99.1, < 3.0)
44
+ ruby-progressbar (~> 1.7)
45
+ unicode-display_width (~> 1.0, >= 1.0.1)
46
+ ruby-progressbar (1.8.1)
47
+ unicode-display_width (1.3.0)
48
+
49
+ PLATFORMS
50
+ ruby
51
+
52
+ DEPENDENCIES
53
+ bundler (~> 1.15)
54
+ fuubar
55
+ nanoc-rust!
56
+ rake
57
+ rspec
58
+ rspec-its (~> 1.2)
59
+ rspec-mocks
60
+ rubocop (~> 0.49)
61
+
62
+ BUNDLED WITH
63
+ 1.15.1
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop/rake_task'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RuboCop::RakeTask.new(:rubocop)
7
+
8
+ RSpec::Core::RakeTask.new(:spec) do |t|
9
+ t.verbose = false
10
+ end
11
+
12
+ task test: %i[spec rubocop]
13
+
14
+ task default: :test
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+
5
+ module NanocRust
6
+ module FFI
7
+ class Edge < ::FFI::Struct
8
+ layout :from, :uint32,
9
+ :to, :uint32,
10
+ :idx, :uint32
11
+
12
+ def to_a(props)
13
+ [self[:from], self[:to], props[self[:idx]]]
14
+ end
15
+ end
16
+
17
+ extend ::FFI::Library
18
+
19
+ root = File.expand_path(File.dirname(__FILE__) + '/..')
20
+ ffi_lib root + '/rust/dist/libnanoc_rust.' + ::FFI::Platform::LIBSUFFIX
21
+
22
+ attach_function :drop_string, %i[pointer], :void
23
+ attach_function :drop_array, %i[pointer uint32], :void
24
+
25
+ attach_function :digraph_create, [], :pointer
26
+ attach_function :digraph_destroy, [:pointer], :void
27
+
28
+ attach_function :digraph_inspect, [:pointer], :pointer
29
+
30
+ attach_function :digraph_add_vertex, %i[pointer string], :void
31
+ attach_function :digraph_add_edge, %i[pointer string string uint32], :void
32
+ attach_function :digraph_delete_edges_to, %i[pointer string], :void
33
+
34
+ attach_function :digraph_direct_predecessors_of, %i[pointer string pointer], :pointer
35
+ attach_function :digraph_predecessors_of, %i[pointer string pointer], :pointer
36
+ attach_function :digraph_direct_successors_of, %i[pointer string pointer], :pointer
37
+ attach_function :digraph_successors_of, %i[pointer string pointer], :pointer
38
+ attach_function :digraph_edges, %i[pointer pointer], :pointer
39
+ attach_function :digraph_vertices, %i[pointer pointer], :pointer
40
+ attach_function :digraph_props_for, %i[pointer pointer pointer], :uint32
41
+ end
42
+
43
+ class DirectedGraph
44
+ def initialize(vertices)
45
+ raw = NanocRust::FFI.digraph_create
46
+ @wrapped = ::FFI::AutoPointer.new(raw, NanocRust::FFI.method(:digraph_destroy))
47
+
48
+ @_props = {}
49
+ @_next_prop_idx = 1
50
+
51
+ vertices.each { |v| add_vertex(v) }
52
+ end
53
+
54
+ def inspect
55
+ ptr = NanocRust::FFI.digraph_inspect(@wrapped)
56
+ s = ptr.read_string
57
+ ::NanocRust::FFI.drop_string(ptr)
58
+ s
59
+ end
60
+
61
+ # mutating
62
+
63
+ def add_vertex(v)
64
+ NanocRust::FFI.digraph_add_vertex(@wrapped, v)
65
+ end
66
+
67
+ def add_edge(v1, v2, props: nil)
68
+ @_props[@_next_prop_idx] = props
69
+ NanocRust::FFI.digraph_add_edge(@wrapped, v1, v2, @_next_prop_idx)
70
+ @_next_prop_idx += 1
71
+ end
72
+
73
+ def delete_edges_to(v)
74
+ NanocRust::FFI.digraph_delete_edges_to(@wrapped, v)
75
+ end
76
+
77
+ # querying
78
+
79
+ def direct_predecessors_of(v)
80
+ count_ptr = new_count_ptr
81
+ ptr = NanocRust::FFI.digraph_direct_predecessors_of(@wrapped, v, count_ptr)
82
+ ptr_to_array_of_strings(ptr, count_ptr)
83
+ end
84
+
85
+ def predecessors_of(v)
86
+ count_ptr = new_count_ptr
87
+ ptr = NanocRust::FFI.digraph_predecessors_of(@wrapped, v, count_ptr)
88
+ ptr_to_array_of_strings(ptr, count_ptr)
89
+ end
90
+
91
+ def direct_successors_of(v)
92
+ count_ptr = new_count_ptr
93
+ ptr = NanocRust::FFI.digraph_direct_successors_of(@wrapped, v, count_ptr)
94
+ ptr_to_array_of_strings(ptr, count_ptr)
95
+ end
96
+
97
+ def successors_of(v)
98
+ count_ptr = new_count_ptr
99
+ ptr = NanocRust::FFI.digraph_successors_of(@wrapped, v, count_ptr)
100
+ ptr_to_array_of_strings(ptr, count_ptr)
101
+ end
102
+
103
+ def vertices
104
+ count_ptr = new_count_ptr
105
+ ptr = NanocRust::FFI.digraph_vertices(@wrapped, count_ptr)
106
+ ptr_to_array_of_u32_tuples(ptr, count_ptr)
107
+ end
108
+
109
+ def edges
110
+ count_ptr = new_count_ptr
111
+ ptr = NanocRust::FFI.digraph_edges(@wrapped, count_ptr)
112
+ ptr_to_array_of_u32_tuples(ptr, count_ptr)
113
+ end
114
+
115
+ def props_for(v1, v2)
116
+ idx = NanocRust::FFI.digraph_props_for(@wrapped, v1, v2)
117
+ @_props[idx]
118
+ end
119
+
120
+ private
121
+
122
+ def new_count_ptr
123
+ ::FFI::MemoryPointer.new(:size_t, 1)
124
+ end
125
+
126
+ def ptr_to_array_of_strings(ptr, count_ptr)
127
+ # Read count
128
+ count = count_ptr.read_uint32
129
+
130
+ # Read strings
131
+ string_ptrs = ptr.read_array_of_pointer(count)
132
+ strings = string_ptrs.map(&:read_string)
133
+
134
+ # Deallocate
135
+ string_ptrs.each { |s| NanocRust::FFI.drop_string(s) }
136
+ NanocRust::FFI.drop_array(ptr, count)
137
+
138
+ strings
139
+ end
140
+
141
+ def ptr_to_array_of_u32_tuples(ptr, count_ptr)
142
+ # Read count
143
+ count = count_ptr.read_uint32
144
+
145
+ # Read edges
146
+ edges_ptr = ::FFI::Pointer.new(NanocRust::FFI::Edge, ptr)
147
+ edges = (0...count).map { |i| NanocRust::FFI::Edge.new(edges_ptr[i]).to_a(@_props) }
148
+
149
+ # Deallocate
150
+ NanocRust::FFI.drop_array(ptr, count)
151
+
152
+ edges
153
+ end
154
+ end
155
+
156
+ def self.activate!
157
+ Nanoc::Int.send(:remove_const, 'DirectedGraph') if Nanoc::Int.const_defined?('DirectedGraph')
158
+ Nanoc::Int.const_set('DirectedGraph', NanocRust::DirectedGraph)
159
+ end
160
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'nanoc-rust'
5
+ s.version = '0.1'
6
+ s.homepage = 'http://nanoc.ws/'
7
+ s.summary = 'Faster bits for Nanoc'
8
+ s.description = 'Contains parts of Nanoc rewritten in Rust for speed'
9
+
10
+ s.author = 'Denis Defreyne'
11
+ s.email = 'denis@stoneship.org'
12
+ s.license = 'MIT'
13
+
14
+ s.files = `git ls-files -z`.split("\x0")
15
+ s.require_paths = ['lib']
16
+
17
+ s.required_ruby_version = '>= 2.3.0'
18
+
19
+ s.extensions = Dir['rust/extconf.rb']
20
+
21
+ s.add_runtime_dependency('ffi', '~> 1.9')
22
+ s.add_development_dependency('bundler', '~> 1.15')
23
+ end
@@ -0,0 +1,4 @@
1
+ [root]
2
+ name = "nanoc_rust"
3
+ version = "0.1.0"
4
+
@@ -0,0 +1,6 @@
1
+ [package]
2
+ name = "nanoc_rust"
3
+ version = "0.1.0"
4
+
5
+ [lib]
6
+ crate-type = ["cdylib"]
@@ -0,0 +1,15 @@
1
+ default: build
2
+
3
+ test:
4
+ cargo test
5
+
6
+ build:
7
+ cargo build --release
8
+ mkdir -p dist
9
+ [ -f target/release/libnanoc_rust.dylib ] && cp target/release/libnanoc_rust.dylib dist || true
10
+ [ -f target/release/libnanoc_rust.so ] && cp target/release/libnanoc_rust.so dist || true
11
+
12
+ clean:
13
+ rm -rf target dist
14
+
15
+ install:
@@ -0,0 +1,3 @@
1
+ unless system('cargo --version') && system('rustc --version')
2
+ raise 'Cannot find rustc/cargo; please install Rust (https://www.rust-lang.org/)'
3
+ end
@@ -0,0 +1,336 @@
1
+ use std::collections::HashMap;
2
+ use std::collections::hash_set;
3
+ use std::collections::HashSet;
4
+ use std::hash::Hash;
5
+ use std::fmt::Debug;
6
+
7
+ pub struct DirectedGraph<T: Eq + Hash> {
8
+ indices: HashMap<T, u32>,
9
+ next_index: u32,
10
+
11
+ vertices: HashSet<T>,
12
+
13
+ froms: HashMap<T, HashSet<T>>,
14
+ tos: HashMap<T, HashSet<T>>,
15
+
16
+ edge_props: HashMap<(T, T), u32>,
17
+
18
+ empty: HashSet<T>,
19
+ }
20
+
21
+ type VertexIter<'a, T> = hash_set::Iter<'a, T>;
22
+
23
+ // TODO: Move to nanoc_rust.rs
24
+ #[repr(C)]
25
+ pub struct Edge {
26
+ from: u32,
27
+ to: u32,
28
+ props: u32,
29
+ }
30
+
31
+ impl<T: Eq + Hash + Clone + Debug> DirectedGraph<T> {
32
+ pub fn new() -> Self {
33
+ DirectedGraph {
34
+ indices: HashMap::new(),
35
+ next_index: 0,
36
+
37
+ froms: HashMap::new(),
38
+ tos: HashMap::new(),
39
+
40
+ edge_props: HashMap::new(),
41
+
42
+ vertices: HashSet::new(),
43
+
44
+ empty: HashSet::new(),
45
+ }
46
+ }
47
+
48
+ // Modifying
49
+
50
+ pub fn add_vertex(&mut self, v: T) {
51
+ self.vertices.insert(v.clone());
52
+ self.indices.entry(v).or_insert(self.next_index);
53
+ self.next_index += 1;
54
+ }
55
+
56
+ pub fn add_edge(&mut self, v1: T, v2: T, props: u32) {
57
+ self.add_vertex(v1.clone());
58
+ self.add_vertex(v2.clone());
59
+
60
+ self.edge_props.insert((v1.clone(), v2.clone()), props);
61
+
62
+ self.tos.entry(v2.clone()).or_insert(HashSet::new()).insert(v1.clone());
63
+ self.froms.entry(v1).or_insert(HashSet::new()).insert(v2);
64
+ }
65
+
66
+ pub fn delete_edges_to(&mut self, v: T) {
67
+ if let Some(mut tos) = self.tos.get_mut(&v) {
68
+ for to in tos.iter() {
69
+ self.edge_props.remove(&(to.clone(), v.clone()));
70
+ }
71
+
72
+ tos.clear();
73
+ }
74
+
75
+ for (_, mut froms) in &mut self.froms {
76
+ froms.remove(&v);
77
+ }
78
+ }
79
+
80
+ // Querying
81
+
82
+ pub fn props_for(&self, v1: T, v2: T) -> u32 {
83
+ let p: Option<&u32> = self.edge_props.get(&(v1, v2));
84
+ if let Some(v) = p.cloned() {
85
+ v
86
+ } else {
87
+ 0
88
+ }
89
+ }
90
+
91
+ pub fn vertices_iter(&self) -> VertexIter<T> {
92
+ self.vertices.iter()
93
+ }
94
+
95
+ pub fn edges(&self) -> Vec<Edge> {
96
+ let mut res: Vec<Edge> = Vec::new();
97
+
98
+ let vertices = self.vertices_iter().collect::<Vec<_>>();
99
+ for from in vertices {
100
+ for to in self.direct_successors_of(from.clone()) {
101
+ res.push(Edge {
102
+ from: *self.indices.get(&from).unwrap(),
103
+ to: *self.indices.get(&to).unwrap(),
104
+ props: self.props_for(from.clone(), to.clone()),
105
+ });
106
+ }
107
+ }
108
+
109
+ res
110
+ }
111
+
112
+ pub fn direct_successors_of(&self, v: T) -> VertexIter<T> {
113
+ let froms = self.froms.get(&v);
114
+ if let Some(froms) = froms {
115
+ froms.iter()
116
+ } else {
117
+ self.empty.iter()
118
+ }
119
+ }
120
+
121
+ pub fn direct_predecessors_of(&self, v: T) -> VertexIter<T> {
122
+ let tos = self.tos.get(&v);
123
+ if let Some(tos) = tos {
124
+ tos.iter()
125
+ } else {
126
+ self.empty.iter()
127
+ }
128
+ }
129
+
130
+ pub fn successors_of(&self, v: T) -> Vec<T> {
131
+ let mut all_vertices: HashSet<T> = HashSet::new();
132
+
133
+ let mut processed_vertices: HashSet<T> = HashSet::new();
134
+ let mut unprocessed_vertices: Vec<T> = vec![v];
135
+
136
+ while let Some(vertex) = unprocessed_vertices.pop() {
137
+ // Get next unprocessed vertex
138
+ if processed_vertices.contains(&vertex) {
139
+ continue;
140
+ }
141
+ processed_vertices.insert(vertex.clone());
142
+
143
+ // Add successors of this vertex
144
+ for v in self.direct_successors_of(vertex) {
145
+ all_vertices.insert(v.clone());
146
+ unprocessed_vertices.push(v.clone());
147
+ }
148
+ }
149
+
150
+ return all_vertices.iter().cloned().collect::<Vec<_>>();
151
+ }
152
+
153
+ pub fn predecessors_of(&self, v: T) -> Vec<T> {
154
+ let mut all_vertices: HashSet<T> = HashSet::new();
155
+
156
+ let mut processed_vertices: HashSet<T> = HashSet::new();
157
+ let mut unprocessed_vertices: Vec<T> = vec![v];
158
+
159
+ while let Some(vertex) = unprocessed_vertices.pop() {
160
+ // Get next unprocessed vertex
161
+ if processed_vertices.contains(&vertex) {
162
+ continue;
163
+ }
164
+ processed_vertices.insert(vertex.clone());
165
+
166
+ // Add predecessors of this vertex
167
+ for v in self.direct_predecessors_of(vertex) {
168
+ all_vertices.insert(v.clone());
169
+ unprocessed_vertices.push(v.clone());
170
+ }
171
+ }
172
+
173
+ return all_vertices.iter().cloned().collect::<Vec<_>>();
174
+ }
175
+ }
176
+
177
+ #[cfg(test)]
178
+ mod tests {
179
+ use super::*;
180
+ use std::fmt::Debug;
181
+ use std::iter::IntoIterator;
182
+
183
+ macro_rules! assert_iters_eq {
184
+ ( $a:expr, $b:expr ) => {{
185
+ match ($a, $b) {
186
+ (a, b) => {
187
+ let mut v1 = a.collect::<Vec<_>>();
188
+ let mut v2 = b.collect::<Vec<_>>();
189
+
190
+ v1.sort();
191
+ v2.sort();
192
+
193
+ assert_eq!(v1, v2);
194
+ }
195
+ }
196
+ }}
197
+ }
198
+
199
+ fn assert_empty<I, T: Debug + Ord>(c: I)
200
+ where
201
+ I: IntoIterator<Item = T>,
202
+ {
203
+ assert_eq!(0, c.into_iter().collect::<Vec<_>>().len());
204
+ }
205
+
206
+ #[test]
207
+ fn new_digraph_is_empty() {
208
+ let dg: DirectedGraph<i32> = DirectedGraph::new();
209
+
210
+ assert_empty(dg.vertices_iter());
211
+
212
+ assert_empty(dg.direct_successors_of(0));
213
+ assert_empty(dg.direct_successors_of(1));
214
+ assert_empty(dg.direct_successors_of(2));
215
+ assert_empty(dg.direct_successors_of(3));
216
+
217
+ assert_empty(dg.direct_predecessors_of(0));
218
+ assert_empty(dg.direct_predecessors_of(1));
219
+ assert_empty(dg.direct_predecessors_of(2));
220
+ assert_empty(dg.direct_predecessors_of(3));
221
+ }
222
+
223
+ #[test]
224
+ fn add_new_vertex() {
225
+ let mut dg: DirectedGraph<i32> = DirectedGraph::new();
226
+ dg.add_vertex(1);
227
+
228
+ assert_eq!(1, dg.vertices_iter().len());
229
+ assert_iters_eq!(vec![1].iter(), dg.vertices_iter());
230
+
231
+ assert_empty(dg.direct_successors_of(0));
232
+ assert_empty(dg.direct_successors_of(1));
233
+ assert_empty(dg.direct_successors_of(2));
234
+ assert_empty(dg.direct_successors_of(3));
235
+
236
+ assert_empty(dg.direct_predecessors_of(0));
237
+ assert_empty(dg.direct_predecessors_of(1));
238
+ assert_empty(dg.direct_predecessors_of(2));
239
+ assert_empty(dg.direct_predecessors_of(3));
240
+ }
241
+
242
+ #[test]
243
+ fn add_existing_vertex() {
244
+ let mut dg: DirectedGraph<i32> = DirectedGraph::new();
245
+ dg.add_vertex(1);
246
+ dg.add_vertex(1);
247
+
248
+ assert_eq!(1, dg.vertices_iter().len());
249
+ assert_iters_eq!(vec![1].iter(), dg.vertices_iter());
250
+
251
+ assert_empty(dg.direct_successors_of(0));
252
+ assert_empty(dg.direct_successors_of(1));
253
+ assert_empty(dg.direct_successors_of(2));
254
+ assert_empty(dg.direct_successors_of(3));
255
+
256
+ assert_empty(dg.direct_predecessors_of(0));
257
+ assert_empty(dg.direct_predecessors_of(1));
258
+ assert_empty(dg.direct_predecessors_of(2));
259
+ assert_empty(dg.direct_predecessors_of(3));
260
+ }
261
+
262
+ #[test]
263
+ fn add_multiple_vertices() {
264
+ let mut dg: DirectedGraph<i32> = DirectedGraph::new();
265
+ dg.add_vertex(1);
266
+ dg.add_vertex(3);
267
+
268
+ assert_eq!(2, dg.vertices_iter().len());
269
+ assert_iters_eq!(vec![1, 3].iter(), dg.vertices_iter());
270
+
271
+ assert_empty(dg.direct_successors_of(0));
272
+ assert_empty(dg.direct_successors_of(1));
273
+ assert_empty(dg.direct_successors_of(2));
274
+ assert_empty(dg.direct_successors_of(3));
275
+
276
+ assert_empty(dg.direct_predecessors_of(0));
277
+ assert_empty(dg.direct_predecessors_of(1));
278
+ assert_empty(dg.direct_predecessors_of(2));
279
+ assert_empty(dg.direct_predecessors_of(3));
280
+ }
281
+
282
+ #[test]
283
+ fn add_edges_has_vertices() {
284
+ let mut dg: DirectedGraph<i32> = DirectedGraph::new();
285
+ dg.add_edge(1, 2);
286
+ dg.add_edge(1, 3);
287
+
288
+ assert_eq!(3, dg.vertices_iter().len());
289
+ assert_iters_eq!(vec![1, 2, 3].iter(), dg.vertices_iter());
290
+ }
291
+
292
+ #[test]
293
+ fn add_edges_has_successors_and_predecessors() {
294
+ let mut dg: DirectedGraph<i32> = DirectedGraph::new();
295
+ dg.add_edge(1, 2);
296
+ dg.add_edge(1, 3);
297
+
298
+ // 0
299
+ assert_empty(dg.direct_successors_of(0));
300
+ assert_empty(dg.direct_predecessors_of(0));
301
+
302
+ // 1
303
+ assert_iters_eq!(vec![2, 3].iter(), dg.direct_successors_of(1));
304
+ assert_empty(dg.direct_predecessors_of(1));
305
+
306
+ // 2
307
+ assert_empty(dg.direct_successors_of(2));
308
+ assert_iters_eq!(vec![1].iter(), dg.direct_predecessors_of(2));
309
+
310
+ // 3
311
+ assert_empty(dg.direct_successors_of(3));
312
+ assert_iters_eq!(vec![1].iter(), dg.direct_predecessors_of(3));
313
+ }
314
+
315
+ #[test]
316
+ fn delete_edges_to() {
317
+ let mut dg: DirectedGraph<i32> = DirectedGraph::new();
318
+ dg.add_edge(0, 1);
319
+ dg.add_edge(0, 2);
320
+ dg.add_edge(0, 3);
321
+ dg.add_edge(1, 2);
322
+ dg.add_edge(1, 3);
323
+ dg.add_edge(2, 3);
324
+ dg.delete_edges_to(1);
325
+
326
+ assert_iters_eq!(vec![2, 3].iter(), dg.direct_successors_of(0));
327
+ assert_iters_eq!(vec![2, 3].iter(), dg.direct_successors_of(1));
328
+ assert_iters_eq!(vec![3].iter(), dg.direct_successors_of(2));
329
+ assert_empty(dg.direct_successors_of(3));
330
+
331
+ assert_empty(dg.direct_predecessors_of(0));
332
+ assert_empty(dg.direct_predecessors_of(1));
333
+ assert_iters_eq!(vec![0, 1].iter(), dg.direct_predecessors_of(2));
334
+ assert_iters_eq!(vec![0, 1, 2].iter(), dg.direct_predecessors_of(3));
335
+ }
336
+ }
@@ -0,0 +1,191 @@
1
+ #![feature(libc)]
2
+
3
+ extern crate libc;
4
+
5
+ mod digraph;
6
+
7
+ pub mod ffi {
8
+ use digraph::{DirectedGraph, Edge};
9
+
10
+ use libc::c_char;
11
+ use std::ffi::{CStr, CString};
12
+ use std::fmt::{Debug, Error, Formatter};
13
+ use std::mem;
14
+ use std::ptr;
15
+
16
+ type R = *const c_char;
17
+ type T = Vec<u8>;
18
+ type D = DirectedGraph<T>;
19
+
20
+ impl Debug for D {
21
+ fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
22
+ write!(f, "Nanoc::Int::DirectedGraph(")?;
23
+
24
+ let mut idx = 0;
25
+ let mut vertices = self.vertices_iter().collect::<Vec<_>>();
26
+ vertices.sort();
27
+ for from in vertices {
28
+ for to in self.direct_successors_of(from.clone()) {
29
+ if idx > 0 {
30
+ write!(f, ", ")?;
31
+ }
32
+
33
+ write!(
34
+ f,
35
+ "{:?} -> {:?} props=nil",
36
+ String::from_utf8_lossy(&*from.clone()),
37
+ String::from_utf8_lossy(&*to.clone()),
38
+ )?;
39
+
40
+ idx += 1;
41
+ }
42
+ }
43
+
44
+ write!(f, ")")?;
45
+ Ok(())
46
+ }
47
+ }
48
+
49
+ // Converting raw <-> actual
50
+
51
+ fn r2t(r: R) -> T {
52
+ unsafe {
53
+ if r.is_null() {
54
+ vec![]
55
+ } else {
56
+ CStr::from_ptr(r).to_bytes().to_vec()
57
+ }
58
+ }
59
+ }
60
+
61
+ fn t2r(t: T) -> R {
62
+ unsafe {
63
+ if t.is_empty() {
64
+ ptr::null()
65
+ } else {
66
+ let cs = CString::from_vec_unchecked(t);
67
+ let ptr = cs.as_ptr();
68
+ mem::forget(cs);
69
+ ptr
70
+ }
71
+ }
72
+ }
73
+
74
+ // drop
75
+
76
+ #[no_mangle]
77
+ pub extern "C" fn drop_string(ptr: *mut c_char) {
78
+ unsafe { CString::from_raw(ptr); }
79
+ }
80
+
81
+ #[no_mangle]
82
+ pub extern "C" fn drop_array(ptr: *mut *mut c_char, length: usize) {
83
+ unsafe { Vec::<*mut c_char>::from_raw_parts(ptr, length, length); };
84
+ }
85
+
86
+ // create+destroy
87
+
88
+ #[no_mangle]
89
+ pub extern "C" fn digraph_create() -> Box<D> {
90
+ Box::new(DirectedGraph::new())
91
+ }
92
+
93
+ #[no_mangle]
94
+ pub extern "C" fn digraph_destroy(dg: &mut D) {
95
+ mem::drop(dg);
96
+ }
97
+
98
+ // query
99
+
100
+ #[no_mangle]
101
+ pub extern "C" fn digraph_inspect(dg: &D) -> R {
102
+ let s = format!("{:?}", dg);
103
+ CString::new(s).unwrap().into_raw()
104
+ }
105
+
106
+ #[no_mangle]
107
+ pub extern "C" fn digraph_props_for(dg: &D, v1: R, v2: R) -> u32 {
108
+ dg.props_for(r2t(v1), r2t(v2))
109
+ }
110
+
111
+ #[no_mangle]
112
+ pub extern "C" fn digraph_edges(dg: &D, out_count: *mut usize) -> *const Edge {
113
+ let mut vec = dg.edges();
114
+ vec.shrink_to_fit();
115
+ let ptr = vec.as_ptr();
116
+ unsafe { ptr::write(out_count, vec.len()); };
117
+ mem::forget(vec);
118
+ ptr
119
+ }
120
+
121
+ #[no_mangle]
122
+ pub extern "C" fn digraph_vertices(dg: &D, out_count: *mut usize) -> *const R {
123
+ let mut vec: Vec<R> = dg.vertices_iter().cloned().map(|a| t2r(a)).collect();
124
+ vec.shrink_to_fit();
125
+ let ptr = vec.as_ptr();
126
+ unsafe { ptr::write(out_count, vec.len()); };
127
+ mem::forget(vec);
128
+ ptr
129
+ }
130
+
131
+ #[no_mangle]
132
+ pub extern "C" fn digraph_direct_predecessors_of(dg: &D, v: R, out_count: *mut usize) -> *const R {
133
+ let mut vec: Vec<R> = dg.direct_predecessors_of(r2t(v)).cloned().map(|a| t2r(a)).collect();
134
+ vec.shrink_to_fit();
135
+
136
+ let ptr = vec.as_ptr();
137
+ unsafe { ptr::write(out_count, vec.len()); };
138
+ mem::forget(vec);
139
+ ptr
140
+ }
141
+
142
+ #[no_mangle]
143
+ pub extern "C" fn digraph_predecessors_of(dg: &D, v: R, out_count: *mut usize) -> *const R {
144
+ let mut vec: Vec<R> = dg.predecessors_of(r2t(v)).iter().cloned().map(|a| t2r(a)).collect();
145
+ vec.shrink_to_fit();
146
+
147
+ let ptr = vec.as_ptr();
148
+ unsafe { ptr::write(out_count, vec.len()); };
149
+ mem::forget(vec);
150
+ ptr
151
+ }
152
+
153
+ #[no_mangle]
154
+ pub extern "C" fn digraph_direct_successors_of(dg: &D, v: R, out_count: *mut usize) -> *const R {
155
+ let mut vec: Vec<R> = dg.direct_successors_of(r2t(v)).cloned().map(|a| t2r(a)).collect();
156
+ vec.shrink_to_fit();
157
+
158
+ let ptr = vec.as_ptr();
159
+ unsafe { ptr::write(out_count, vec.len()); };
160
+ mem::forget(vec);
161
+ ptr
162
+ }
163
+
164
+ #[no_mangle]
165
+ pub extern "C" fn digraph_successors_of(dg: &D, v: R, out_count: *mut usize) -> *const R {
166
+ let mut vec: Vec<R> = dg.successors_of(r2t(v)).iter().cloned().map(|a| t2r(a)).collect();
167
+ vec.shrink_to_fit();
168
+
169
+ let ptr = vec.as_ptr();
170
+ unsafe { ptr::write(out_count, vec.len()); };
171
+ mem::forget(vec);
172
+ ptr
173
+ }
174
+
175
+ // mutate
176
+
177
+ #[no_mangle]
178
+ pub extern "C" fn digraph_add_vertex(dg: &mut D, v: R) {
179
+ dg.add_vertex(r2t(v))
180
+ }
181
+
182
+ #[no_mangle]
183
+ pub extern "C" fn digraph_add_edge(dg: &mut D, v1: R, v2: R, p: u32) {
184
+ dg.add_edge(r2t(v1), r2t(v2), p)
185
+ }
186
+
187
+ #[no_mangle]
188
+ pub extern "C" fn digraph_delete_edges_to(dg: &mut D, v: R) {
189
+ dg.delete_edges_to(r2t(v));
190
+ }
191
+ }
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe NanocRust::DirectedGraph do
4
+ subject(:graph) { described_class.new(%w[1 2 3]) }
5
+
6
+ describe '#edges' do
7
+ subject { graph.edges }
8
+
9
+ context 'empty graph' do
10
+ it { is_expected.to be_empty }
11
+ end
12
+
13
+ context 'graph with vertices, but no edges' do
14
+ before do
15
+ graph.add_vertex('1')
16
+ graph.add_vertex('2')
17
+ end
18
+
19
+ it { is_expected.to be_empty }
20
+ end
21
+
22
+ context 'graph with edges from previously added vertices' do
23
+ before do
24
+ graph.add_vertex('1')
25
+ graph.add_vertex('2')
26
+ graph.add_vertex('3')
27
+
28
+ graph.add_edge('1', '2')
29
+ graph.add_edge('1', '3')
30
+ end
31
+
32
+ it { is_expected.to match_array([[0, 1, nil], [0, 2, nil]]) }
33
+ end
34
+
35
+ context 'graph with edges from new vertices' do
36
+ before do
37
+ graph.add_edge('1', '2')
38
+ graph.add_edge('1', '3')
39
+ end
40
+
41
+ it { is_expected.to match_array([[0, 1, nil], [0, 2, nil]]) }
42
+ end
43
+
44
+ context 'graph with edge props' do
45
+ before do
46
+ graph.add_edge('1', '2', props: { name: 'Mr. C' })
47
+ graph.add_edge('1', '3', props: { name: 'Cooper' })
48
+ end
49
+
50
+ it { is_expected.to match_array([[0, 1, { name: 'Mr. C' }], [0, 2, { name: 'Cooper' }]]) }
51
+ end
52
+ end
53
+
54
+ describe '#props_for' do
55
+ subject { graph.props_for('1', '2') }
56
+
57
+ context 'no edge' do
58
+ it { is_expected.to be_nil }
59
+ end
60
+
61
+ context 'edge, but no props' do
62
+ before { graph.add_edge('1', '2') }
63
+ it { is_expected.to be_nil }
64
+ end
65
+
66
+ context 'edge with props' do
67
+ before { graph.add_edge('1', '2', props: { name: 'Mr. C' }) }
68
+ it { is_expected.to eq(name: 'Mr. C') }
69
+
70
+ context 'deleted edge (#delete_edges_to)' do
71
+ before { graph.delete_edges_to('2') }
72
+ it { is_expected.to be_nil }
73
+ end
74
+ end
75
+ end
76
+
77
+ describe '#direct_predecessors_of' do
78
+ subject { graph.direct_predecessors_of('2') }
79
+
80
+ context 'no edges' do
81
+ it { is_expected.to be_empty }
82
+ end
83
+
84
+ context 'one edge to' do
85
+ before { graph.add_edge('1', '2') }
86
+ it { is_expected.to eq(['1']) }
87
+ end
88
+
89
+ context 'two edges to' do
90
+ before do
91
+ graph.add_edge('1', '2')
92
+ graph.add_edge('3', '2')
93
+ end
94
+
95
+ it { is_expected.to match_array(%w[1 3]) }
96
+ end
97
+
98
+ context 'edge from' do
99
+ before { graph.add_edge('2', '3') }
100
+ it { is_expected.to be_empty }
101
+ end
102
+ end
103
+
104
+ describe '#direct_successors_of' do
105
+ subject { graph.direct_successors_of('2') }
106
+
107
+ context 'no edges' do
108
+ it { is_expected.to be_empty }
109
+ end
110
+
111
+ context 'one edge to' do
112
+ before { graph.add_edge('1', '2') }
113
+ it { is_expected.to be_empty }
114
+ end
115
+
116
+ context 'one edge from' do
117
+ before { graph.add_edge('2', '3') }
118
+ it { is_expected.to eq(['3']) }
119
+ end
120
+
121
+ context 'two edges from' do
122
+ before do
123
+ graph.add_edge('2', '1')
124
+ graph.add_edge('2', '3')
125
+ end
126
+
127
+ it { is_expected.to match_array(%w[1 3]) }
128
+ end
129
+ end
130
+
131
+ describe '#predecessors_of' do
132
+ subject { graph.predecessors_of('2') }
133
+
134
+ context 'no predecessors' do
135
+ before do
136
+ graph.add_edge('2', '3')
137
+ end
138
+
139
+ it { is_expected.to be_empty }
140
+ end
141
+
142
+ context 'direct predecessor' do
143
+ before do
144
+ graph.add_edge('2', '3')
145
+ graph.add_edge('1', '2')
146
+ end
147
+
148
+ context 'no indirect predecessors' do
149
+ it { is_expected.to match_array(['1']) }
150
+ end
151
+
152
+ context 'indirect predecessors' do
153
+ before { graph.add_edge('3', '1') }
154
+ it { is_expected.to match_array(%w[1 2 3]) }
155
+ end
156
+ end
157
+ end
158
+
159
+ describe '#successors_of' do
160
+ subject { graph.successors_of('2') }
161
+
162
+ context 'no successors' do
163
+ before do
164
+ graph.add_edge('1', '2')
165
+ end
166
+
167
+ it { is_expected.to be_empty }
168
+ end
169
+
170
+ context 'direct predecessor' do
171
+ before do
172
+ graph.add_edge('1', '2')
173
+ graph.add_edge('2', '3')
174
+ end
175
+
176
+ context 'no indirect successors' do
177
+ it { is_expected.to match_array(['3']) }
178
+ end
179
+
180
+ context 'indirect successors' do
181
+ before { graph.add_edge('3', '1') }
182
+ it { is_expected.to match_array(%w[1 2 3]) }
183
+ end
184
+ end
185
+ end
186
+
187
+ describe '#inspect' do
188
+ subject { graph.inspect }
189
+
190
+ context 'empty graph' do
191
+ it { is_expected.to eq('Nanoc::Int::DirectedGraph()') }
192
+ end
193
+
194
+ context 'one edge, no props' do
195
+ before do
196
+ graph.add_edge('1', '2')
197
+ end
198
+
199
+ it { is_expected.to eq('Nanoc::Int::DirectedGraph("1" -> "2" props=nil)') }
200
+ end
201
+
202
+ context 'two edges, no props' do
203
+ before do
204
+ graph.add_edge('1', '2')
205
+ graph.add_edge('2', '3')
206
+ end
207
+
208
+ it { is_expected.to eq('Nanoc::Int::DirectedGraph("1" -> "2" props=nil, "2" -> "3" props=nil)') }
209
+ end
210
+
211
+ context 'one edge, props' do
212
+ before do
213
+ graph.add_edge('1', '2', props: 'giraffe')
214
+ end
215
+
216
+ it { is_expected.to eq('Nanoc::Int::DirectedGraph("1" -> "2" props="giraffe")') }
217
+ end
218
+
219
+ context 'two edges, props' do
220
+ before do
221
+ graph.add_edge('1', '2', props: 'donkey')
222
+ graph.add_edge('2', '3', props: 'zebra')
223
+ end
224
+
225
+ it { is_expected.to eq('Nanoc::Int::DirectedGraph("1" -> "2" props="donkey", "2" -> "3" props="zebra")') }
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/its'
4
+ require 'fuubar'
5
+
6
+ require 'nanoc-rust'
7
+
8
+ RSpec.configure do |c|
9
+ c.fuubar_progress_bar_options = {
10
+ format: '%c/%C |<%b>%i| %p%%',
11
+ }
12
+ end
data/test.sh ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euxo pipefail
4
+
5
+ make -C rust test
6
+ gem build nanoc-rust.gemspec
7
+ gem uninstall -x nanoc-rust
8
+ gem install nanoc-rust-0.1.gem
9
+ ruby bin/nanoc-rust
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nanoc-rust
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Denis Defreyne
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-07-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ffi
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.9'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.9'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.15'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.15'
41
+ description: Contains parts of Nanoc rewritten in Rust for speed
42
+ email: denis@stoneship.org
43
+ executables: []
44
+ extensions:
45
+ - rust/extconf.rb
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".gitignore"
49
+ - ".rspec"
50
+ - ".rubocop.yml"
51
+ - Gemfile
52
+ - Gemfile.lock
53
+ - Rakefile
54
+ - lib/nanoc-rust.rb
55
+ - nanoc-rust.gemspec
56
+ - rust/Cargo.lock
57
+ - rust/Cargo.toml
58
+ - rust/Makefile
59
+ - rust/extconf.rb
60
+ - rust/src/digraph.rs
61
+ - rust/src/nanoc_rust.rs
62
+ - spec/directed_graph_spec.rb
63
+ - spec/spec_helper.rb
64
+ - test.sh
65
+ homepage: http://nanoc.ws/
66
+ licenses:
67
+ - MIT
68
+ metadata: {}
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: 2.3.0
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubyforge_project:
85
+ rubygems_version: 2.6.12
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: Faster bits for Nanoc
89
+ test_files: []