Naseweis 0.0.1 → 0.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 +9 -0
- data/.rubocop.yml +23 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +4 -0
- data/Naseweis.gemspec +36 -0
- data/README.md +1 -1
- data/Rakefile +2 -0
- data/WEISHEIT.md +184 -0
- data/lib/naseweis.rb +160 -0
- data/lib/naseweis/converter.rb +69 -0
- data/lib/naseweis/version.rb +4 -0
- metadata +13 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1daa455db84c7312b2526ca7387ec6d9675abd6c
|
4
|
+
data.tar.gz: 5612fb9fe210a801e9ff364aca8f7828c9f9cc24
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7c99b14fbc4933df198f039494b7ec8e9a3d56dfcd18001558df2979ca1c1f1fb0be57adec31bc62855b230d8df81f5d2dca9dcd4646cd1257348662743ddda8
|
7
|
+
data.tar.gz: 80af614856fe8c7fa21f5446668976074e41d97fb436a10665e0be9842daa8f5540de58f2f13d746c42e09f5d016095830e8fd3dcd472f4fdd948786d7decf51
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
AllCops:
|
2
|
+
Include:
|
3
|
+
- 'lib'
|
4
|
+
Exclude:
|
5
|
+
- 'Naseweis.gemspec'
|
6
|
+
|
7
|
+
Style/TrailingCommaInArguments:
|
8
|
+
EnforcedStyleForMultiline: comma
|
9
|
+
|
10
|
+
Style/TrailingCommaInLiteral:
|
11
|
+
EnforcedStyleForMultiline: comma
|
12
|
+
|
13
|
+
Metrics/MethodLength:
|
14
|
+
Max: 30
|
15
|
+
|
16
|
+
Metrics/CyclomaticComplexity:
|
17
|
+
Max: 8
|
18
|
+
|
19
|
+
Metrics/PerceivedComplexity:
|
20
|
+
Max: 9
|
21
|
+
|
22
|
+
Metrics/AbcSize:
|
23
|
+
Max: 20
|
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
- WEISHEIT.md CHANGELOG.md
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Naseweis.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'naseweis/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "Naseweis"
|
8
|
+
spec.version = Naseweis::VERSION
|
9
|
+
spec.authors = ["Daniel Schadt"]
|
10
|
+
spec.email = ["kingdread@gmx.de"]
|
11
|
+
|
12
|
+
spec.summary = "Gather lots of information based on questionnaire files."
|
13
|
+
spec.description = <<-EOF
|
14
|
+
Naseweis is a library that allows you to gather information based on
|
15
|
+
questions which are defined in yaml files. This lets you keep your data and
|
16
|
+
logic separated and avoids cluttering your code with many calls to
|
17
|
+
puts/gets. It also allows you to keep your questions organized, centralized
|
18
|
+
and language-agnostic.
|
19
|
+
EOF
|
20
|
+
spec.homepage = "https://github.com/Kingdread/Naseweis"
|
21
|
+
spec.license = "MIT"
|
22
|
+
|
23
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
24
|
+
spec.bindir = "exe"
|
25
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
26
|
+
spec.require_paths = ["lib"]
|
27
|
+
|
28
|
+
spec.extra_rdoc_files = ["README.md", "WEISHEIT.md", "CHANGELOG.md"]
|
29
|
+
spec.rdoc_options << "--title" << "Naseweis Documentation" <<
|
30
|
+
"--main" << "README.md"
|
31
|
+
|
32
|
+
spec.add_development_dependency "bundler", "~> 1.11"
|
33
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
34
|
+
|
35
|
+
spec.add_runtime_dependency "highline", "~> 1.7"
|
36
|
+
end
|
data/README.md
CHANGED
@@ -33,7 +33,7 @@ result["times"].times { puts "Hello, #{name}" }
|
|
33
33
|
The `Weisheits` format is a normal YAML file, which defines questions, their
|
34
34
|
target name, their type, and some more information.
|
35
35
|
|
36
|
-
The full description can be found in WEISHEIT.md.
|
36
|
+
The full description can be found in {file:WEISHEIT.md WEISHEIT.md}.
|
37
37
|
|
38
38
|
## Installation
|
39
39
|
|
data/Rakefile
ADDED
data/WEISHEIT.md
CHANGED
@@ -0,0 +1,184 @@
|
|
1
|
+
# `Weisheits`-files
|
2
|
+
|
3
|
+
A `Weisheits`-fiile is a YAML file that describes the questions that should be
|
4
|
+
asked.
|
5
|
+
|
6
|
+
The top level element should be a list of questions, whereas the syntax of a
|
7
|
+
question is described below.
|
8
|
+
|
9
|
+
## question objects
|
10
|
+
|
11
|
+
A question is a simple dictionary of various "modifiers", which allow you to
|
12
|
+
customize the behaviour.
|
13
|
+
|
14
|
+
Available options are:
|
15
|
+
|
16
|
+
### `q`
|
17
|
+
|
18
|
+
The actual question as a string, which is then used as a prompt, or a list of
|
19
|
+
subquestions. If this attribute is a string, then the input will be saved,
|
20
|
+
otherwise a dictionary of the sub-question responses will be saved.
|
21
|
+
|
22
|
+
#### Examples
|
23
|
+
|
24
|
+
```yaml
|
25
|
+
# Simple string question
|
26
|
+
- q: "What is your name?"
|
27
|
+
target: user_name
|
28
|
+
|
29
|
+
# Subquestions
|
30
|
+
- target: user_data
|
31
|
+
q:
|
32
|
+
- q: "User name?"
|
33
|
+
target: name
|
34
|
+
- q: "User email?"
|
35
|
+
target: email
|
36
|
+
```
|
37
|
+
|
38
|
+
In the first case, the result is available via `result["user_name"]`, in the
|
39
|
+
second case, the name is saved as `result["user_data"]["name"]` and
|
40
|
+
`result["user_data"]["email"]`.
|
41
|
+
|
42
|
+
### `target`
|
43
|
+
|
44
|
+
The name of the target variable, which will contain the user data.
|
45
|
+
|
46
|
+
If the question is a normal string, the data is saved as a string. If the
|
47
|
+
question has subquestions, the data is a hash, which contains the subquestions
|
48
|
+
data. If the question is repeated, the data is saved as a list of separate
|
49
|
+
inputs. If the question has a type specified, it will be type-converted.
|
50
|
+
|
51
|
+
### `desc`
|
52
|
+
|
53
|
+
Description of the question, which will be printed before the question is
|
54
|
+
asked. This is useful e.g. for repeating questions, as it will be displayed
|
55
|
+
once (while the prompt will be displayed multiple times). It can also be used
|
56
|
+
as a "print" function if no question data is gathered.
|
57
|
+
|
58
|
+
#### Examples
|
59
|
+
|
60
|
+
```yaml
|
61
|
+
- desc: "Just print something"
|
62
|
+
|
63
|
+
- desc: "Gather some lines, input an empty line to finish"
|
64
|
+
repeat: true
|
65
|
+
target: lines
|
66
|
+
```
|
67
|
+
|
68
|
+
### `repeat`
|
69
|
+
|
70
|
+
Define if the question should be repeated. A repeated question will save its
|
71
|
+
result as a list. The prompt is displayed at each iteration, if you only want
|
72
|
+
to display it once, use `desc` instead.
|
73
|
+
|
74
|
+
There are multiple ways a question can be repeated:
|
75
|
+
|
76
|
+
* `repeat: true`: repeat until an empty line is entered.
|
77
|
+
* `repeat: 3`: repeat 3 times.
|
78
|
+
* `repeat: "Continue?"`: ask the given prompt, if it is answered with "yes",
|
79
|
+
repeat the question again.
|
80
|
+
|
81
|
+
Note that the presence of the `repeat` attribute is enough to force the answer
|
82
|
+
to be a list, even if the actual question produces 0 or 1 inputs.
|
83
|
+
|
84
|
+
#### Examples
|
85
|
+
|
86
|
+
```yaml
|
87
|
+
- desc: "Enter your address, end with an empty line"
|
88
|
+
repeat: true
|
89
|
+
target: address
|
90
|
+
|
91
|
+
- prompt: "Your sibling's name?"
|
92
|
+
target: siblings
|
93
|
+
repeat: "Do you have another sibling?"
|
94
|
+
```
|
95
|
+
|
96
|
+
### `type`
|
97
|
+
|
98
|
+
Type of the question. This is used to both verify the input and convert it to
|
99
|
+
the native Ruby type.
|
100
|
+
|
101
|
+
Valid types are:
|
102
|
+
|
103
|
+
* `int`, `integer`: Integer
|
104
|
+
* `float`: floating point number
|
105
|
+
* `regex`, `regexp`: valid regular expression
|
106
|
+
|
107
|
+
### `choices`
|
108
|
+
|
109
|
+
A list of valid choices. The user can select one of the given items, but they
|
110
|
+
can not define their own.
|
111
|
+
|
112
|
+
#### Examples
|
113
|
+
|
114
|
+
```yaml
|
115
|
+
- q: "Pick your starter"
|
116
|
+
choices: ["Charmander", "Bulbasaur", "Squirtle"]
|
117
|
+
|
118
|
+
- q: "Pick your language"
|
119
|
+
choices:
|
120
|
+
- Ruby
|
121
|
+
- Python
|
122
|
+
- Perl
|
123
|
+
```
|
124
|
+
|
125
|
+
## Nesting questions
|
126
|
+
|
127
|
+
Questions can be nested arbitrarily deep, if you want to make an address book,
|
128
|
+
you could do something like
|
129
|
+
|
130
|
+
```yaml
|
131
|
+
- desc: "Fill your address book!"
|
132
|
+
repeat: "Add another contact?"
|
133
|
+
target: contacts
|
134
|
+
q:
|
135
|
+
- q: "Contact name?"
|
136
|
+
target: name
|
137
|
+
- q: "Contact address?"
|
138
|
+
target: address
|
139
|
+
- desc: "Additional information (end with an empty line)"
|
140
|
+
repeat: true
|
141
|
+
target: information
|
142
|
+
```
|
143
|
+
|
144
|
+
Processing the file will lead to the following interaction:
|
145
|
+
|
146
|
+
```
|
147
|
+
>>> Fill your address book!
|
148
|
+
>>> Contact name?
|
149
|
+
Darth Vader
|
150
|
+
>>> Contact address?
|
151
|
+
Death Star
|
152
|
+
>>> Additional information (end with an empty line)
|
153
|
+
Very nice guy!
|
154
|
+
|
155
|
+
>>> Add another contact?
|
156
|
+
yes
|
157
|
+
>>> Contact name?
|
158
|
+
Luke Skywalker
|
159
|
+
>>> Contact address?
|
160
|
+
Dagobah
|
161
|
+
>>> Additional information (end with an empty line)
|
162
|
+
|
163
|
+
>>> Add another contact?
|
164
|
+
no
|
165
|
+
```
|
166
|
+
|
167
|
+
And finally to the Ruby structure:
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
{
|
171
|
+
"contacts"=>[
|
172
|
+
{
|
173
|
+
"name"=>"Darth Vader",
|
174
|
+
"address"=>"Death Star",
|
175
|
+
"information"=>["Very nice guy!"]
|
176
|
+
},
|
177
|
+
{
|
178
|
+
"name"=>"Luke Skywalker",
|
179
|
+
"address"=>"Dagobah",
|
180
|
+
"information"=>[]
|
181
|
+
}
|
182
|
+
]
|
183
|
+
}
|
184
|
+
```
|
data/lib/naseweis.rb
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'naseweis/converter'
|
2
|
+
require 'naseweis/version'
|
3
|
+
require 'yaml'
|
4
|
+
require 'highline'
|
5
|
+
|
6
|
+
# The Naseweis module is a module which takes a +Weisheits+-file (or short
|
7
|
+
# +Weisfile+) and asks the user the questions that are defined in the
|
8
|
+
# +Weisfile+.
|
9
|
+
#
|
10
|
+
# This is useful if you have an application that needs to ask a lot of
|
11
|
+
# questions and you'd rather keep the questions (the data) out of the program
|
12
|
+
# (the logic).
|
13
|
+
#
|
14
|
+
# This module helps by defining a "mini-language", which can be used to specify
|
15
|
+
# questions and later retrieve their answers to process them.
|
16
|
+
#
|
17
|
+
# For more information about the file format see the +Weisfile+ document.
|
18
|
+
module Naseweis
|
19
|
+
# Exception raised when the input file (+Weisheits+-file) is malformed
|
20
|
+
class WeisheitError < StandardError
|
21
|
+
end
|
22
|
+
|
23
|
+
# A class to read a +Weisfile+ and gather user input
|
24
|
+
#
|
25
|
+
# @attr_reader [String] filename The path to the file which is used by this
|
26
|
+
# {Nase}
|
27
|
+
# @attr_reader [Array] questions All questions handled by this {Nase}.
|
28
|
+
#
|
29
|
+
# To update the questions, use the {#read} method.
|
30
|
+
# @attr_reader [Converter] converter The converter that is used to convert
|
31
|
+
# types
|
32
|
+
class Nase
|
33
|
+
attr_reader :filename, :questions, :converter
|
34
|
+
|
35
|
+
# Create a new {Nase} which reads questions from the given file
|
36
|
+
#
|
37
|
+
# @param path [String] path to the file with the questions
|
38
|
+
def initialize(path)
|
39
|
+
@filename = path
|
40
|
+
@questions = {}
|
41
|
+
@converter = Converter.new
|
42
|
+
end
|
43
|
+
|
44
|
+
# Update the questions and re-read them from the file that the Nase was
|
45
|
+
# initialized with
|
46
|
+
#
|
47
|
+
# @return [void]
|
48
|
+
# @raise [WeisheitError] if the input file is malformed
|
49
|
+
def read
|
50
|
+
questions = YAML.load_file(@filename)
|
51
|
+
verify questions
|
52
|
+
@questions = questions
|
53
|
+
end
|
54
|
+
|
55
|
+
# Check whether the given question is wellformed
|
56
|
+
#
|
57
|
+
# @param q [Hash,Array] the question or list of questions to check
|
58
|
+
# @return [void]
|
59
|
+
# @raise [WeisheitError] if the question is malformed
|
60
|
+
def verify(q)
|
61
|
+
# Currently only checks if the question type is valid
|
62
|
+
if q.is_a? Array
|
63
|
+
q.each { |x| verify x }
|
64
|
+
return
|
65
|
+
end
|
66
|
+
type = q['type']
|
67
|
+
qs = q['q']
|
68
|
+
well = type.nil? || @converter.supported_types.include?(type.intern)
|
69
|
+
raise WeisheitError, "invalid type #{type}" unless well
|
70
|
+
verify qs if qs.is_a? Array
|
71
|
+
end
|
72
|
+
|
73
|
+
# Start the question session and return the user answers
|
74
|
+
#
|
75
|
+
# @param instream [File] input stream, i.e. stream where data is read from
|
76
|
+
# @param outstream [File] output stream, i.e. stream where prompts are
|
77
|
+
# printed to
|
78
|
+
# @return [Hash] Hash of the user answers, where the keys are defined by
|
79
|
+
# the question file.
|
80
|
+
def interrogate(instream: $stdin, outstream: $stdout)
|
81
|
+
@io = HighLine.new instream, outstream
|
82
|
+
ask @questions
|
83
|
+
@io = nil
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
# Ask the given list of questions and return the answers as a Hash
|
89
|
+
#
|
90
|
+
# @param questions [Array] list of questions to ask
|
91
|
+
# @return [Hash] Hash of the user answers
|
92
|
+
def ask(questions)
|
93
|
+
namespace = {}
|
94
|
+
questions.each do |q|
|
95
|
+
answer = do_question q
|
96
|
+
namespace[q['target']] = answer if q.key?('target') && !answer.nil?
|
97
|
+
end
|
98
|
+
namespace
|
99
|
+
end
|
100
|
+
|
101
|
+
# Handle a single question and return the answer
|
102
|
+
#
|
103
|
+
# @param q [Hash] the question data
|
104
|
+
# @return [String] for a simple question
|
105
|
+
# @return [Array] for a repeating question
|
106
|
+
def do_question(q)
|
107
|
+
if q.key? 'desc'
|
108
|
+
# Always output the description first
|
109
|
+
@io.say q['desc']
|
110
|
+
end
|
111
|
+
|
112
|
+
repeat = q['repeat']
|
113
|
+
return get_valid_input q if repeat.nil?
|
114
|
+
return (1..repeat).collect { get_valid_input q } if repeat.is_a? Integer
|
115
|
+
if repeat.is_a? String
|
116
|
+
result = [get_valid_input(q)]
|
117
|
+
result.push(get_valid_input(q)) while @io.agree repeat
|
118
|
+
else
|
119
|
+
result = []
|
120
|
+
loop do
|
121
|
+
line = get_valid_input q
|
122
|
+
break if line.empty?
|
123
|
+
result << line
|
124
|
+
end
|
125
|
+
end
|
126
|
+
result
|
127
|
+
end
|
128
|
+
|
129
|
+
# Get a single line of user input that is valid for the given question
|
130
|
+
#
|
131
|
+
# @param q [Hash] the question which to get input for
|
132
|
+
# @return [String] if the question is a simple question
|
133
|
+
# @return [Hash] if the question has sub-questions
|
134
|
+
# @return [Object] if the question is type-converted
|
135
|
+
def get_valid_input(q)
|
136
|
+
prompt = q['q']
|
137
|
+
prompt = '' if prompt.nil?
|
138
|
+
result = nil
|
139
|
+
loop do
|
140
|
+
if prompt.is_a? Array
|
141
|
+
result = ask prompt
|
142
|
+
elsif !q['choices'].nil?
|
143
|
+
@io.say prompt
|
144
|
+
result = @io.choose(*q['choices'])
|
145
|
+
else
|
146
|
+
result = @io.ask prompt
|
147
|
+
end
|
148
|
+
break if q['type'].nil?
|
149
|
+
begin
|
150
|
+
result = @converter.convert result, q['type']
|
151
|
+
rescue ConversionError
|
152
|
+
@io.say "invalid value for type #{q['type']}"
|
153
|
+
else
|
154
|
+
break
|
155
|
+
end
|
156
|
+
end
|
157
|
+
result
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Naseweis
|
2
|
+
# A +ConversionError+ is raised when the given data can't be converted to the
|
3
|
+
# requested type. It acts as a common error to catch all other errors that
|
4
|
+
# are raised by Ruby when converting between types.
|
5
|
+
#
|
6
|
+
# @attr_reader [String] data the data that was attempted to convert
|
7
|
+
# @attr_reader [String] type the typename that was requested
|
8
|
+
class ConversionError < StandardError
|
9
|
+
attr_reader :data, :type
|
10
|
+
|
11
|
+
# Create a new ConversionError
|
12
|
+
#
|
13
|
+
# @param data [String] value for {#data}
|
14
|
+
# @param type [String] value for {#type}
|
15
|
+
def initialize(data, type)
|
16
|
+
@data = data
|
17
|
+
@type = type
|
18
|
+
end
|
19
|
+
|
20
|
+
# Get the string representation for the error
|
21
|
+
#
|
22
|
+
# @return [String] error string
|
23
|
+
def to_s
|
24
|
+
"Can't convert '#{@data}' to #{type}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# The +Converter+ class provides a way to convert between stringy data and
|
29
|
+
# native Ruby types. It's used to handle the +type:+ attribute of questions.
|
30
|
+
#
|
31
|
+
# @attr_reader [Hash] converters A hash of all available types.
|
32
|
+
class Converter
|
33
|
+
attr_reader :converters
|
34
|
+
|
35
|
+
def initialize
|
36
|
+
@converters = {
|
37
|
+
int: ->(x) { Integer x },
|
38
|
+
integer: ->(x) { Integer x },
|
39
|
+
regex: ->(x) { Regexp.new x },
|
40
|
+
regexp: ->(x) { Regexp.new x },
|
41
|
+
float: ->(x) { Float x },
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
# Convert the data to the given target type
|
46
|
+
#
|
47
|
+
# @param data [String] the question answer
|
48
|
+
# @param type [String] the target type
|
49
|
+
# @return [Object] the converted data
|
50
|
+
# @raise [ConversionError] if the data cannot be converted to the given
|
51
|
+
# target
|
52
|
+
def convert(data, type)
|
53
|
+
type = type.intern
|
54
|
+
raise ArgumentError, "Invalid type #{type}" unless @converters.key? type
|
55
|
+
begin
|
56
|
+
@converters[type][data]
|
57
|
+
rescue
|
58
|
+
raise ConversionError.new data, type
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Get a list of all supported types
|
63
|
+
#
|
64
|
+
# @return [Array] an array of supported types
|
65
|
+
def supported_types
|
66
|
+
@converters.keys
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: Naseweis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Schadt
|
@@ -65,9 +65,20 @@ extensions: []
|
|
65
65
|
extra_rdoc_files:
|
66
66
|
- README.md
|
67
67
|
- WEISHEIT.md
|
68
|
+
- CHANGELOG.md
|
68
69
|
files:
|
70
|
+
- ".gitignore"
|
71
|
+
- ".rubocop.yml"
|
72
|
+
- ".yardopts"
|
73
|
+
- CHANGELOG.md
|
74
|
+
- Gemfile
|
75
|
+
- Naseweis.gemspec
|
69
76
|
- README.md
|
77
|
+
- Rakefile
|
70
78
|
- WEISHEIT.md
|
79
|
+
- lib/naseweis.rb
|
80
|
+
- lib/naseweis/converter.rb
|
81
|
+
- lib/naseweis/version.rb
|
71
82
|
homepage: https://github.com/Kingdread/Naseweis
|
72
83
|
licenses:
|
73
84
|
- MIT
|
@@ -97,3 +108,4 @@ signing_key:
|
|
97
108
|
specification_version: 4
|
98
109
|
summary: Gather lots of information based on questionnaire files.
|
99
110
|
test_files: []
|
111
|
+
has_rdoc:
|