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 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: