nanoc-rust 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []