talk 2.0.1 → 2.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/.travis.yml +6 -0
- data/Gemfile +10 -0
- data/README.md +6 -0
- data/Rakefile +9 -0
- data/features/class-field.feature +226 -0
- data/features/class.feature +95 -0
- data/features/enumeration-constant.feature +76 -0
- data/features/enumeration.feature +35 -0
- data/features/glossary-term.feature +46 -0
- data/features/glossary.feature +35 -0
- data/features/step_definitions/class-field.rb +74 -0
- data/features/step_definitions/class.rb +50 -0
- data/features/step_definitions/enumeration-constant.rb +42 -0
- data/features/step_definitions/enumeration.rb +29 -0
- data/features/step_definitions/glossary-term.rb +31 -0
- data/features/step_definitions/glossary.rb +23 -0
- data/features/support/env.rb +261 -0
- data/lib/context.rb +282 -0
- data/lib/context_class.rb +224 -0
- data/lib/contexts/README.md +274 -0
- data/lib/contexts/base.rb +6 -0
- data/lib/contexts/boolean.rb +5 -0
- data/lib/contexts/class.rb +11 -0
- data/lib/contexts/constant.rb +5 -0
- data/lib/contexts/enumeration.rb +20 -0
- data/lib/contexts/field.rb +47 -0
- data/lib/contexts/glossary.rb +7 -0
- data/lib/contexts/inherits.rb +3 -0
- data/lib/contexts/map.rb +7 -0
- data/lib/contexts/meta.rb +2 -0
- data/lib/contexts/method.rb +15 -0
- data/lib/contexts/numeric.rb +5 -0
- data/lib/contexts/protocol.rb +8 -0
- data/lib/contexts/reference.rb +6 -0
- data/lib/contexts/string.rb +5 -0
- data/lib/contexts/target.rb +11 -0
- data/lib/contexts/term.rb +11 -0
- data/lib/languages/java/java.rb +145 -0
- data/lib/languages/java/templates/class.java.erb +22 -0
- data/lib/languages/java/templates/enumeration.java.erb +10 -0
- data/lib/languages/java/templates/glossary.java.erb +8 -0
- data/lib/languages/language.rb +172 -0
- data/lib/languages/objc/objc.rb +162 -0
- data/lib/languages/objc/templates/TalkClasses.h.erb +3 -0
- data/lib/languages/objc/templates/TalkClassesForward.h.erb +2 -0
- data/lib/languages/objc/templates/TalkConstants.h.erb +22 -0
- data/lib/languages/objc/templates/TalkObjectList.h.erb +4 -0
- data/lib/languages/objc/templates/class.h.erb +21 -0
- data/lib/languages/objc/templates/class.m.erb +43 -0
- data/lib/parse_error.rb +4 -0
- data/lib/parser.rb +119 -0
- data/lib/registry.rb +151 -0
- data/lib/talk.rb +5 -0
- data/talk.gemspec +18 -0
- metadata +71 -3
@@ -0,0 +1,22 @@
|
|
1
|
+
<%= autogenerated_warning %>
|
2
|
+
<% @base[:glossary].each do |glossary| %>
|
3
|
+
|
4
|
+
// @glossary <%= glossary[:name] %>
|
5
|
+
<%= comment_block(glossary) %>
|
6
|
+
|
7
|
+
<% glossary[:term].each do |term| %>
|
8
|
+
<%= comment_block(term) %>
|
9
|
+
#define <%= glossary_term_name(term[:name]) %> @"<%= term[:value] %>"
|
10
|
+
<% end # term %>
|
11
|
+
|
12
|
+
<% end # glossary %>
|
13
|
+
|
14
|
+
<% @base[:enumeration].each do |enumeration| %>
|
15
|
+
|
16
|
+
<%= comment_block(enumeration) %>
|
17
|
+
enum <%= truncated_name(enumeration[:name]) %>
|
18
|
+
{
|
19
|
+
<%= (enumeration[:constant].map { |constant| "\t"+constant_definition(constant) }).join("\n") %>
|
20
|
+
};
|
21
|
+
|
22
|
+
<% end # enumeration %>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<%= autogenerated_warning %>
|
2
|
+
#import "TalkObjectList.h"
|
3
|
+
|
4
|
+
<%= comment_block(@current_class) %>
|
5
|
+
@interface <%= "#{truncated_name(@current_class)}" %> : <%= "#{superclass(@current_class)}" %>
|
6
|
+
{
|
7
|
+
<%=
|
8
|
+
lines = []
|
9
|
+
@current_class[:field].each do |field|
|
10
|
+
lines.push comment_block(field, 1)
|
11
|
+
lines.push "\t"+field_definition(@current_class, field) + "; // " + field[:type].join('')
|
12
|
+
lines.push ""
|
13
|
+
end
|
14
|
+
|
15
|
+
lines.join("\n")
|
16
|
+
%>}
|
17
|
+
|
18
|
+
<%= (@current_class[:field].map { |f| "@property #{field_tags(f)} #{field_definition(@current_class, f)};" }).join("\n") %>
|
19
|
+
+(NSString *) fullyQualifiedClassName;
|
20
|
+
|
21
|
+
@end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
<%= autogenerated_warning %>
|
2
|
+
#define __CANONFILE__ "<%= truncated_name(@current_class) %>"
|
3
|
+
#import "TalkObjectList.h"
|
4
|
+
#import "TalkObject+Private.h"
|
5
|
+
|
6
|
+
#if ! __has_feature(objc_arc)
|
7
|
+
#error This file requires Automatic Reference Counting
|
8
|
+
#endif
|
9
|
+
|
10
|
+
@implementation <%= truncated_name(@current_class) %>
|
11
|
+
<%= dynamic_body_for_named_wrapper %>
|
12
|
+
<% unless trimmed_fields(@current_class).empty? then %>
|
13
|
+
@synthesize <%= (trimmed_fields(@current_class).map { |f| mapped_name(@current_class, f[:name], :field) }).join(', ') %>;
|
14
|
+
<% end %>
|
15
|
+
|
16
|
+
-(void) initKeymaps
|
17
|
+
{
|
18
|
+
[super initKeymaps];
|
19
|
+
<% @target[:map].each do |map|
|
20
|
+
next unless map[:class_name] == truncated_name(@current_class[:name])%>
|
21
|
+
[self mapSerializedKey:@"<%= map[:field_name] %>" toLocalKey:@"<%= map[:new_field_name] %>"];
|
22
|
+
<% end %>}
|
23
|
+
|
24
|
+
<% unless (@current_class[:field].reject { |f| assist_line(f).nil? }).length == 0 then %>
|
25
|
+
-(NSString *) containerStructureForLocalKey: (NSString *) localKey
|
26
|
+
{<%
|
27
|
+
@current_class[:field].each do |field|
|
28
|
+
assist = assist_line(field)
|
29
|
+
unless assist.nil? %>
|
30
|
+
if([localKey isEqualToString:@"<%= mapped_name(@current_class, field[:name], :field) %>"]) return @"<%= assist %>";
|
31
|
+
<%
|
32
|
+
end
|
33
|
+
end
|
34
|
+
%>
|
35
|
+
return [super containerStructureForLocalKey:localKey];
|
36
|
+
}
|
37
|
+
<% end %>
|
38
|
+
+(NSString *) fullyQualifiedClassName
|
39
|
+
{
|
40
|
+
return @"<%= @current_class[:name] %>";
|
41
|
+
}
|
42
|
+
|
43
|
+
@end
|
data/lib/parse_error.rb
ADDED
data/lib/parser.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require "parse_error.rb"
|
2
|
+
require "context"
|
3
|
+
|
4
|
+
module Talk
|
5
|
+
attr_reader :contexts, :finalized
|
6
|
+
|
7
|
+
@contexts = {}
|
8
|
+
|
9
|
+
class Parser
|
10
|
+
attr_accessor :basepath
|
11
|
+
|
12
|
+
def self.error(tag, file, line, message)
|
13
|
+
near_msg = tag.nil? ? "" : " near @#{tag}"
|
14
|
+
raise ParseError, "#{file}:#{line} parse error#{near_msg}: #{message}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@contexts = [ Context.context_for_name(:base).new("base", "n/a", "0") ]
|
19
|
+
@base = @contexts.first
|
20
|
+
@finalized = false
|
21
|
+
@closed_contexts = []
|
22
|
+
end
|
23
|
+
|
24
|
+
# signal that we are done parsing files, and it is time to do final validation
|
25
|
+
def finalize
|
26
|
+
close_active_context until @contexts.empty?
|
27
|
+
@closed_contexts.each { |ctx| ctx.finalize }
|
28
|
+
finalized = true
|
29
|
+
end
|
30
|
+
|
31
|
+
def results
|
32
|
+
finalize unless @finalized
|
33
|
+
@base.to_h
|
34
|
+
end
|
35
|
+
|
36
|
+
def trim_filename(filename)
|
37
|
+
if @basepath.nil? == false and filename.start_with? @basepath then
|
38
|
+
filename = filename[@basepath.length .. -1]
|
39
|
+
filename = filename[1..-1] while filename.start_with? "/"
|
40
|
+
end
|
41
|
+
|
42
|
+
filename
|
43
|
+
end
|
44
|
+
|
45
|
+
def parse_file(filename)
|
46
|
+
parse(filename, IO.read(filename))
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse(filename, contents)
|
50
|
+
contents = contents.split("\n") unless contents.is_a? Array
|
51
|
+
contents.each_with_index { |line, line_num| parse_line(line.strip.split, filename, line_num+1) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def parse_line(words, file, line)
|
55
|
+
return if line_is_comment?(words)
|
56
|
+
|
57
|
+
@file = trim_filename(file)
|
58
|
+
@line = line
|
59
|
+
words.each { |word| parse_word(word) }
|
60
|
+
end
|
61
|
+
|
62
|
+
def parse_word(word)
|
63
|
+
@word = word
|
64
|
+
|
65
|
+
if word_is_tag?(word) then
|
66
|
+
parse_tag(identifier_from_tag_word(word))
|
67
|
+
else
|
68
|
+
@contexts.last.parse(word, @file, @line)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def parse_tag(tag)
|
73
|
+
@tag = tag
|
74
|
+
@contexts.last.has_tag?(tag) ? parse_supported_tag : parse_unsupported_tag
|
75
|
+
end
|
76
|
+
|
77
|
+
def parse_supported_tag
|
78
|
+
new_context = @contexts.last.start_tag(@tag, @file, @line)
|
79
|
+
if new_context.nil? then
|
80
|
+
close_active_context
|
81
|
+
else
|
82
|
+
@contexts.push new_context
|
83
|
+
@closed_contexts.push new_context
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def parse_unsupported_tag
|
88
|
+
stack = Array.new(@contexts)
|
89
|
+
stack.pop until stack.empty? or stack.last.has_tag? @tag
|
90
|
+
|
91
|
+
parse_error("tag not supported in @#{@contexts.last.tag.to_s}") if stack.empty?
|
92
|
+
|
93
|
+
close_active_context until @contexts.last == stack.last
|
94
|
+
|
95
|
+
parse_supported_tag
|
96
|
+
end
|
97
|
+
|
98
|
+
def close_active_context
|
99
|
+
@closed_contexts.push @contexts.pop
|
100
|
+
@contexts.last.end_tag(@closed_contexts.last) unless @contexts.empty?
|
101
|
+
end
|
102
|
+
|
103
|
+
def word_is_tag?(word)
|
104
|
+
word[0] == '@'
|
105
|
+
end
|
106
|
+
|
107
|
+
def identifier_from_tag_word(word)
|
108
|
+
word[1..-1].to_sym
|
109
|
+
end
|
110
|
+
|
111
|
+
def line_is_comment?(line)
|
112
|
+
line.length > 0 and line[0].length > 0 and line[0][0] == '#'
|
113
|
+
end
|
114
|
+
|
115
|
+
def parse_error(message)
|
116
|
+
raise Talk::Parser.error(@tag, @file, @line, message)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
data/lib/registry.rb
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
class Array
|
2
|
+
def each_prefix
|
3
|
+
self.length.times do |i|
|
4
|
+
yield(self[0..i])
|
5
|
+
end
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
module Talk
|
10
|
+
class RegistryEntry
|
11
|
+
attr_reader :file, :line, :name
|
12
|
+
|
13
|
+
def initialize(name=nil, file=nil, line=nil)
|
14
|
+
@file = file
|
15
|
+
@line = line
|
16
|
+
@name = name
|
17
|
+
@children = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def make_entry(name, file, line)
|
21
|
+
@name = name
|
22
|
+
@file = file
|
23
|
+
@line = line
|
24
|
+
end
|
25
|
+
|
26
|
+
def [](key)
|
27
|
+
@children[key]
|
28
|
+
end
|
29
|
+
|
30
|
+
def []=(key, value)
|
31
|
+
@children[key] = value
|
32
|
+
end
|
33
|
+
|
34
|
+
def keys
|
35
|
+
@children.keys
|
36
|
+
end
|
37
|
+
|
38
|
+
def each(&block)
|
39
|
+
@children.each &block
|
40
|
+
end
|
41
|
+
|
42
|
+
def has_children?
|
43
|
+
not @children.empty?
|
44
|
+
end
|
45
|
+
|
46
|
+
def is_entry?
|
47
|
+
@file != nil
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_s
|
51
|
+
is_entry? ? "#{@file}:#{@line}" : "container"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class Registry
|
56
|
+
class << self
|
57
|
+
def add(name, namespace, file, line, delimiter=nil)
|
58
|
+
if registered?(name, namespace) then
|
59
|
+
old_reg = get_registrations(name, namespace).last
|
60
|
+
Talk::Parser.error(nil, file, line, "Duplicate registration #{name} in #{namespace}; previously defined at #{old_reg}")
|
61
|
+
end
|
62
|
+
|
63
|
+
@registry ||= {}
|
64
|
+
@registry[namespace] ||= {}
|
65
|
+
split_name = delimiter.nil? ? [*name] : name.to_s.split(delimiter)
|
66
|
+
|
67
|
+
# create each level of the split name
|
68
|
+
level = @registry[namespace]
|
69
|
+
split_name[0..-2].each do |component| # all but the last component
|
70
|
+
level[component] ||= RegistryEntry.new
|
71
|
+
level = level[component]
|
72
|
+
end
|
73
|
+
|
74
|
+
level[split_name.last] ||= RegistryEntry.new(name, file, line)
|
75
|
+
level[split_name.last].make_entry(name, file, line) # in case it already existed as a container
|
76
|
+
add_reverse_lookup(split_name, namespace, level[split_name.last], delimiter)
|
77
|
+
end
|
78
|
+
|
79
|
+
def add_reverse_lookup(split_name, namespace, entry, delimiter)
|
80
|
+
@reverse ||= {}
|
81
|
+
@reverse[namespace] ||= {}
|
82
|
+
split_name.reverse.each_prefix do |prefix|
|
83
|
+
name = prefix.reverse.join(delimiter)
|
84
|
+
@reverse[namespace][name] ||= []
|
85
|
+
@reverse[namespace][name].push entry
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def get_registrations(name, namespace, match_exact = false)
|
90
|
+
@registry ||= {}
|
91
|
+
exact = get_exact_registrations(name, namespace)
|
92
|
+
|
93
|
+
return exact unless exact.empty?
|
94
|
+
return [] if match_exact
|
95
|
+
|
96
|
+
get_inexact_registrations(name, namespace)
|
97
|
+
end
|
98
|
+
|
99
|
+
def get_exact_registrations(name, namespace)
|
100
|
+
level = @registry[namespace]
|
101
|
+
[*name].each do |component|
|
102
|
+
return [] if level.nil?
|
103
|
+
level = level[component]
|
104
|
+
end
|
105
|
+
|
106
|
+
return [*level] if not level.nil? and level.is_entry?
|
107
|
+
[]
|
108
|
+
end
|
109
|
+
|
110
|
+
def get_inexact_registrations(name, namespace)
|
111
|
+
return [] if @reverse.nil? or @reverse[namespace].nil? or @reverse[namespace][name].nil?
|
112
|
+
@reverse[namespace][name]
|
113
|
+
end
|
114
|
+
|
115
|
+
def registered?(name, namespace, match_exact = false)
|
116
|
+
not get_registrations(name, namespace, match_exact).empty?
|
117
|
+
end
|
118
|
+
|
119
|
+
def render_level(level, at_depth=1)
|
120
|
+
indent = " "*at_depth
|
121
|
+
s = ""
|
122
|
+
level.keys.sort.each do |key|
|
123
|
+
value = level[key]
|
124
|
+
s += indent + key
|
125
|
+
s += " (entry from #{File.basename(value.file)}:#{value.line})" if value.is_entry?
|
126
|
+
s += "\n"
|
127
|
+
s += render_level(value, at_depth+1)
|
128
|
+
end
|
129
|
+
|
130
|
+
s
|
131
|
+
end
|
132
|
+
|
133
|
+
def reset
|
134
|
+
@registry = nil
|
135
|
+
@reverse = nil
|
136
|
+
end
|
137
|
+
|
138
|
+
def to_s
|
139
|
+
return "Empty registry" if @registry.nil? or @registry.empty?
|
140
|
+
s = ""
|
141
|
+
@registry.keys.sort.each do |namespace|
|
142
|
+
level = @registry[namespace]
|
143
|
+
s += "Namespace #{namespace}:\n"
|
144
|
+
s += render_level(level)
|
145
|
+
end
|
146
|
+
|
147
|
+
s
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
data/lib/talk.rb
ADDED
data/talk.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'talk'
|
3
|
+
s.executables << 'talk'
|
4
|
+
s.version = '2.0.2'
|
5
|
+
s.date = '2014-05-14'
|
6
|
+
s.summary = "Compile-to-source protocol contract specification language"
|
7
|
+
s.description = "A lightweight language for specifying protocol contracts. Compiles to source in Java, Javascript, ObjC and Ruby."
|
8
|
+
s.authors = ["Jonas Acres"]
|
9
|
+
s.email = 'jonas@becuddle.com'
|
10
|
+
s.homepage = 'http://github.com/jonasacres/talk'
|
11
|
+
s.license = 'GPLv2'
|
12
|
+
|
13
|
+
s.files = `git ls-files`.split($/)
|
14
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
15
|
+
s.test_files = s.files.grep(%r{^(test|s|features)/})
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
|
18
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: talk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.
|
4
|
+
version: 2.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jonas Acres
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-05-14 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: A lightweight language for specifying protocol contracts. Compiles to
|
14
14
|
source in Java, Javascript, ObjC and Ruby.
|
@@ -18,7 +18,62 @@ executables:
|
|
18
18
|
extensions: []
|
19
19
|
extra_rdoc_files: []
|
20
20
|
files:
|
21
|
+
- .gitignore
|
22
|
+
- .travis.yml
|
23
|
+
- Gemfile
|
24
|
+
- README.md
|
25
|
+
- Rakefile
|
21
26
|
- bin/talk
|
27
|
+
- features/class-field.feature
|
28
|
+
- features/class.feature
|
29
|
+
- features/enumeration-constant.feature
|
30
|
+
- features/enumeration.feature
|
31
|
+
- features/glossary-term.feature
|
32
|
+
- features/glossary.feature
|
33
|
+
- features/step_definitions/class-field.rb
|
34
|
+
- features/step_definitions/class.rb
|
35
|
+
- features/step_definitions/enumeration-constant.rb
|
36
|
+
- features/step_definitions/enumeration.rb
|
37
|
+
- features/step_definitions/glossary-term.rb
|
38
|
+
- features/step_definitions/glossary.rb
|
39
|
+
- features/support/env.rb
|
40
|
+
- lib/context.rb
|
41
|
+
- lib/context_class.rb
|
42
|
+
- lib/contexts/README.md
|
43
|
+
- lib/contexts/base.rb
|
44
|
+
- lib/contexts/boolean.rb
|
45
|
+
- lib/contexts/class.rb
|
46
|
+
- lib/contexts/constant.rb
|
47
|
+
- lib/contexts/enumeration.rb
|
48
|
+
- lib/contexts/field.rb
|
49
|
+
- lib/contexts/glossary.rb
|
50
|
+
- lib/contexts/inherits.rb
|
51
|
+
- lib/contexts/map.rb
|
52
|
+
- lib/contexts/meta.rb
|
53
|
+
- lib/contexts/method.rb
|
54
|
+
- lib/contexts/numeric.rb
|
55
|
+
- lib/contexts/protocol.rb
|
56
|
+
- lib/contexts/reference.rb
|
57
|
+
- lib/contexts/string.rb
|
58
|
+
- lib/contexts/target.rb
|
59
|
+
- lib/contexts/term.rb
|
60
|
+
- lib/languages/java/java.rb
|
61
|
+
- lib/languages/java/templates/class.java.erb
|
62
|
+
- lib/languages/java/templates/enumeration.java.erb
|
63
|
+
- lib/languages/java/templates/glossary.java.erb
|
64
|
+
- lib/languages/language.rb
|
65
|
+
- lib/languages/objc/objc.rb
|
66
|
+
- lib/languages/objc/templates/TalkClasses.h.erb
|
67
|
+
- lib/languages/objc/templates/TalkClassesForward.h.erb
|
68
|
+
- lib/languages/objc/templates/TalkConstants.h.erb
|
69
|
+
- lib/languages/objc/templates/TalkObjectList.h.erb
|
70
|
+
- lib/languages/objc/templates/class.h.erb
|
71
|
+
- lib/languages/objc/templates/class.m.erb
|
72
|
+
- lib/parse_error.rb
|
73
|
+
- lib/parser.rb
|
74
|
+
- lib/registry.rb
|
75
|
+
- lib/talk.rb
|
76
|
+
- talk.gemspec
|
22
77
|
homepage: http://github.com/jonasacres/talk
|
23
78
|
licenses:
|
24
79
|
- GPLv2
|
@@ -43,4 +98,17 @@ rubygems_version: 2.0.14
|
|
43
98
|
signing_key:
|
44
99
|
specification_version: 4
|
45
100
|
summary: Compile-to-source protocol contract specification language
|
46
|
-
test_files:
|
101
|
+
test_files:
|
102
|
+
- features/class-field.feature
|
103
|
+
- features/class.feature
|
104
|
+
- features/enumeration-constant.feature
|
105
|
+
- features/enumeration.feature
|
106
|
+
- features/glossary-term.feature
|
107
|
+
- features/glossary.feature
|
108
|
+
- features/step_definitions/class-field.rb
|
109
|
+
- features/step_definitions/class.rb
|
110
|
+
- features/step_definitions/enumeration-constant.rb
|
111
|
+
- features/step_definitions/enumeration.rb
|
112
|
+
- features/step_definitions/glossary-term.rb
|
113
|
+
- features/step_definitions/glossary.rb
|
114
|
+
- features/support/env.rb
|