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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ba21648ac300ae3282685dd78622cd657aa643be
4
- data.tar.gz: 3b7416d60d3a9e9ab28777b0e5bf69c73b714814
3
+ metadata.gz: 1daa455db84c7312b2526ca7387ec6d9675abd6c
4
+ data.tar.gz: 5612fb9fe210a801e9ff364aca8f7828c9f9cc24
5
5
  SHA512:
6
- metadata.gz: 334a84249548f107b84dbdd691200ecbb3827ea064c2531292189015316655e02b7bfc465439940f8a9ccee03971739ae73a1d7f3a6407bb673e48ca8648791a
7
- data.tar.gz: 6cf1c4adc993f537db46927d9f01ea06c72674da182822c3a7a874274ae0549538c6df227b92201c68d0dc357a1ddd0876b98ac4b1bf04ee889f6a9cd7dd803a
6
+ metadata.gz: 7c99b14fbc4933df198f039494b7ec8e9a3d56dfcd18001558df2979ca1c1f1fb0be57adec31bc62855b230d8df81f5d2dca9dcd4646cd1257348662743ddda8
7
+ data.tar.gz: 80af614856fe8c7fa21f5446668976074e41d97fb436a10665e0be9842daa8f5540de58f2f13d746c42e09f5d016095830e8fd3dcd472f4fdd948786d7decf51
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
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
@@ -0,0 +1,5 @@
1
+ # 0.0.2 (unreleased)
2
+
3
+ * Add `regex` and `float` types
4
+ * Add options for `interrogate` to select the streams
5
+ * Verify the questions in `read` to prevent errors popping up later
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in Naseweis.gemspec
4
+ gemspec
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
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
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
@@ -0,0 +1,4 @@
1
+ module Naseweis
2
+ # Version of the module
3
+ VERSION = '0.0.2'.freeze
4
+ 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.1
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: