Naseweis 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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:
|