resme 0.4.0 → 0.5.1

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
  SHA256:
3
- metadata.gz: d8abd7d17f0d2b9a777cba680826c21d6ee882f3f3df0c74ba086e1d617d4b6b
4
- data.tar.gz: b34b33c2f2d0f3beae3bef713efba2e39b05537d8a208380e6392271e0cbcb09
3
+ metadata.gz: de3845cee948da0b1588b074a720662d5ea22a8c1cb1176aad2fa09f6f7cab3f
4
+ data.tar.gz: 83ae22f7d7731406fdd3940e97c9e39c9a61b07336e95f2efc369cae719687f9
5
5
  SHA512:
6
- metadata.gz: fe74aa311a4dd110c26a0760cf6d2f8cb1ba1ee8e587f2a64d00ff6aa420a2a7f6c37b1eec392ddf1b28abbe49ffb94d6b26d5f43f2797ba9fd7cc9874ebf5c1
7
- data.tar.gz: 3f2ae60c5c8a4e7f8135aa7f43aafa081f913dc8dd6c1b4c6065606447a41b6afe414ae4f32e0541fc06284227d94628f31f59e80af02aee2147c054032137c8
6
+ metadata.gz: 32d94c5d909f54888d2a8fa184aaa05f8a9d8075d6f3f19ff6654bf23e58f82e54e96d84ea08de148548ed0260396c6d9790b6dbc0e3992c295157b5713bb952
7
+ data.tar.gz: 41386481b15b5b14ba86efbd23b29331341b22f91ebcc70497d8293609414a15e8c06518ad441e0cd26f78b552a47e7d125c42bc495f17def2bbd16314fe52b5
data/CHANGELOG.org ADDED
@@ -0,0 +1,60 @@
1
+ #+TITLE: CHANGELOG
2
+ #+AUTHOR: Adolfo Villafiorita
3
+
4
+ * Version 0.5.1
5
+
6
+ A bug-fixing release.
7
+
8
+ More in details:
9
+
10
+ - [bug] Add dependency to =classy_hash=
11
+ - [doc] Fix links to source code and Changelog.
12
+ - [doc] Merge Release History and Changelog.
13
+ - [doc] Fix version numbers in headers of Changelog (1.5.0 -> 0.5.0)
14
+
15
+ * Version 0.5.0
16
+ - [user] New command =view= allows to view the template used
17
+ for generating a resume in a specific format
18
+ - [user] Check command is now based on ClassyHash
19
+ - [code] Various changes to the code
20
+
21
+ * Version 0.4.0
22
+
23
+ Version 0.4.0 refactors all generation commands under =generate=, provides
24
+ new filtering options, adds =-e= option (for custom templates), and
25
+ refactors various portions of code. It also revises this document
26
+ and fixes some minor bugs.
27
+
28
+ More in details:
29
+
30
+ - [user] New option =--skip= allows to skip some top-level sections.
31
+ You mileage might vary, as some formats might require the
32
+ information you are trying to skip
33
+ - [user] New command =generate= is now used to generate the Resume
34
+ in different formats.
35
+ - [user] New option =--erb= allows to specify a custom template for
36
+ generating the resume. Use it in conjunction with =view= (released
37
+ in version 1.5.0) to jump-start your layout.
38
+
39
+ * Version 0.3.2 and 0.3.1
40
+
41
+ Version 0.3.2 and 0.3.1 fix errors with the Europass format: lists of
42
+ projects, interests, ... are now properly formatted.
43
+
44
+ * Version 0.3
45
+
46
+ Version 0.3 Introduces output to org-mode, introduces references for the CV,
47
+ improves output to JSON, adds a =check= command, removes useless blank
48
+ lines in the output, supports =-%>= in the ERB templates, fixes
49
+ various typos in the documentation, introduces various new formatting
50
+ functions, to simplify template generation
51
+
52
+ * Version 0.2
53
+
54
+ Version 0.2 improves output of volunteering activities and other information
55
+ in the Europass and *significantly improves error and warning
56
+ reporting*
57
+
58
+ * Version 0.1
59
+
60
+ Version 0.1 is the first release
data/Gemfile.lock ADDED
@@ -0,0 +1,20 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ resme (0.5.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ rake (12.3.3)
10
+
11
+ PLATFORMS
12
+ x86_64-linux
13
+
14
+ DEPENDENCIES
15
+ bundler (~> 2.3.20)
16
+ rake (~> 12.0)
17
+ resme!
18
+
19
+ BUNDLED WITH
20
+ 2.3.20
data/README.org CHANGED
@@ -17,20 +17,20 @@ needs).
17
17
 
18
18
  Add this line to your application's Gemfile:
19
19
 
20
- #+BEGIN_SRC ruby
21
- gem 'resme'
22
- #+END_SRC
20
+ #+begin_example ruby
21
+ gem 'resme'
22
+ #+end_example
23
23
 
24
24
  And then execute:
25
25
 
26
- #+BEGIN_EXAMPLE
27
- $ bundle
28
- #+END_EXAMPLE
26
+ #+begin_example sh
27
+ bundle
28
+ #+end_example
29
29
 
30
30
  Or install it yourself as:
31
31
 
32
- #+BEGIN_EXAMPLE
33
- $ gem install resme
32
+ #+begin_example sh
33
+ gem install resme
34
34
  #+END_EXAMPLE
35
35
 
36
36
  ** Usage
@@ -40,9 +40,9 @@ Or install it yourself as:
40
40
 
41
41
  Start with:
42
42
 
43
- #+BEGIN_EXAMPLE
43
+ #+begin_example sh
44
44
  resme init
45
- #+END_EXAMPLE
45
+ #+end_example
46
46
 
47
47
  which generates a YML template for your resume in the current directory.
48
48
  Comments in the YML file should help you fill the various entries.
@@ -51,7 +51,7 @@ are not relevant for your resume.
51
51
 
52
52
  You can now generate a resume using one of the existing formats:
53
53
 
54
- #+begin_example
54
+ #+begin_example sh
55
55
  resme --to org resume.yml
56
56
  #+end_example
57
57
 
@@ -64,18 +64,33 @@ Supported formats include:
64
64
  - =json=: JSON format (https://jsonresume.org/)
65
65
 
66
66
  If you are not satisfied with the provided templates, you can write
67
- your own (see below). In this case, however, =resme= is mainly an ERB
68
- renderer.
67
+ your own (see below).
69
68
 
70
- Remarks:
69
+ Notice that you can specify more than one YML file in input. This allows you to
70
+ store data about your resume in different files, if you like to do so
71
+ (e.g., work experiences could be in one file and talks in another).
72
+ The YML files are merged before processing them.
71
73
 
72
- - You can specify more than one YML file in input. This allows you to
73
- store data about your resume in different files, if you like to do so
74
- (e.g., work experiences could be in one file and talks in another).
75
- The YML files are merged before processing them.
76
- - The output filename is optional. If you do not specify one, the resume
77
- is generated to =resume-YYYY.MM.DD.format=, where =YYYY-MM-DD= is
78
- today's date and =format= is the chosen output format
74
+ Full syntax:
75
+
76
+ #+begin_src shell :results raw output :wrap example
77
+ resme help
78
+ #+end_src
79
+
80
+ #+RESULTS:
81
+ #+begin_example
82
+ resme command [options] [args]
83
+ Available commands:
84
+ check resume.yml # Check syntax of resume.yml
85
+ list resume.yml # List main sections in resume.yml
86
+ version # print version information
87
+ view --template FORMAT # Print template used for format
88
+ generate [options] resume.yml ... # output resume
89
+ console # enter the console
90
+ man # print resme manual page
91
+ help [command] # print command usage
92
+ init [options] # generate an empty resume.yml file
93
+ #+end_example
79
94
 
80
95
  ** Checking validity
81
96
  :PROPERTIES:
@@ -85,9 +100,9 @@ Remarks:
85
100
  Use the =check= command to verify whether your YAML file conforms with
86
101
  the intended syntax.
87
102
 
88
- #+BEGIN_SRC ruby
103
+ #+begin_example sh
89
104
  resme check resume.yaml
90
- #+END_SRC
105
+ #+end_example
91
106
 
92
107
  ** Dates in the resume
93
108
  :PROPERTIES:
@@ -110,28 +125,36 @@ The third and the forth format allows you to enter "partial" dates
110
125
  :END:
111
126
 
112
127
  The resumes are generated from the YML matter using ERB templates. The
113
- provided output formats should support different back ends (Org Mode
114
- and Markdown easily allow for generation of PDFs, HTML, and ODT, to
115
- mention a few).
116
-
117
- However, if you want, you can define your own templates. All the data
118
- in the resume is made available in the =data= variable. Thus, for
119
- instance, the following code snippets generates a list of all the work
120
- experiences:
121
-
122
- #+BEGIN_EXAMPLE
123
- <% data.work each do |exp| %>
124
- - <%= exp.who %>
125
- From: <%= exp.from %> till: <%= exp.till %>
128
+ provided output formats should support different back ends. Org Mode
129
+ and Markdown, for instance, easily allow for generation of PDFs, HTML,
130
+ and ODT, to mention a few.
131
+
132
+ However, if you want, you can define your own templates.
133
+
134
+ Use the command =view= to print one of the templates used by =resme=
135
+ and build from that.
136
+
137
+ #+begin_example sh
138
+ resme view --template md
139
+ #+end_example
140
+
141
+ Notice that all the data in the resume is made available in the =data=
142
+ variable. Thus, for instance, the following code snippets generates a
143
+ list of all the work experiences:
144
+
145
+ #+begin_example xml
146
+ <% data["work"] each do |exp| %>
147
+ - <%= exp["who"] %>
148
+ From: <%= exp["from"] %> till: <%= exp["till"] %>
126
149
  <% end %>
127
- #+END_EXAMPLE
150
+ #+end_example
128
151
 
129
- To specify your own ERB template use the option =-e= (=--erb=). Thus,
130
- for instance:
152
+ To specify your own ERB template to build your resume use the option
153
+ =-e= (=--erb=). Thus, for instance:
131
154
 
132
- #+BEGIN_EXAMPLE
133
- $ resme render -e template.md.erb [-o output_filename] file.yaml ...
134
- #+END_EXAMPLE
155
+ #+begin_example sh
156
+ resme render -e template.md.erb [-o output_filename] file.yaml ...
157
+ #+end_example
135
158
 
136
159
  uses =template.md.erb= to generate the resume.
137
160
 
@@ -200,12 +223,16 @@ https://github.com/avillafiorita/resme.
200
223
  The gem is available as open source under the terms of the
201
224
  [[http://opensource.org/licenses/MIT][MIT License]].
202
225
 
226
+ ** Change Log
227
+
228
+ In [[file:./CHANGELOG.org][CHANGELOG.org]]
229
+
203
230
  ** Roadmap
204
231
  :PROPERTIES:
205
232
  :CUSTOM_ID: roadmap
206
233
  :END:
207
234
 
208
- In =doc/todo.org= ... guess what is my preferred editor!
235
+ In [[file:./doc/todo.org][todo.org]] ... guess what is my preferred editor!
209
236
 
210
237
  ** Bugs
211
238
  :PROPERTIES:
@@ -228,18 +255,4 @@ Unknown number of unknown bugs.
228
255
  :CUSTOM_ID: release-history
229
256
  :END:
230
257
 
231
- - *0.4.0* refactors all generation commands under =generate=, provides
232
- new filtering options, adds =-e= option (for custom templates), and
233
- refactors various portions of code. It also revises this document
234
- and fixes some minor bugs.
235
- - *0.3.2* and *0.3.1* fix errors with the Europass format: lists of
236
- projects, interests, ... are now properly formatted.
237
- - *0.3* introduces output to org-mode, introduces references for the CV,
238
- improves output to JSON, adds a =check= command, removes useless blank
239
- lines in the output, supports =-%>= in the ERB templates, fixes
240
- various typos in the documentation, introduces various new formatting
241
- functions, to simplify template generation
242
- - *0.2* improves output of volunteering activities and other information
243
- in the Europass and *significantly improves error and warning
244
- reporting*
245
- - *0.1* is the first release
258
+ Moved to [[file:./CHANGELOG.org][CHANGELOG.org]].
@@ -5,7 +5,6 @@ require "date"
5
5
  require "yaml"
6
6
  require "erb"
7
7
  require "json"
8
- require "kwalify"
9
8
 
10
9
  module Resme
11
10
  module CommandSemantics
@@ -93,24 +92,24 @@ module Resme
93
92
  # APP SPECIFIC COMMANDS
94
93
  #
95
94
  def self.check(opts, argv)
96
- path = File.join(File.dirname(__FILE__), "/../templates/schema.yml")
97
- schema = Kwalify::Yaml.load_file(path)
98
-
99
- # create validator
100
- validator = Kwalify::Validator.new(schema)
101
- # load document
102
- document = Kwalify::Yaml.load_file(argv[0])
103
- # validate
104
- errors = validator.validate(document)
105
-
106
- # show errors
107
- if errors && !errors.empty?
108
- for e in errors
109
- puts "[#{e.path}] #{e.message}"
95
+ begin
96
+ document = YAML.load_file(argv[0], permitted_classes: [Date])
97
+ rescue Psych::SyntaxError => ex
98
+ puts "The file #{argv[0]} has an invalid structure."
99
+ puts ex.message
100
+ exit 1
101
+ end
102
+
103
+ begin
104
+ errors = ResumeStructureValidator.validate(document)
105
+ rescue Exception => ex
106
+ puts "The files #{argv[0]} does not validate"
107
+ ex.entries.each do |error|
108
+ puts "#{error[:full_path]}: #{error[:message]}"
110
109
  end
111
- else
112
- puts "The file #{argv[0]} validates."
110
+ exit 1
113
111
  end
112
+ puts "The file #{argv[0]} has a valid structure."
114
113
  end
115
114
 
116
115
  def self.init(opts, argv)
@@ -141,6 +140,12 @@ module Resme
141
140
  end
142
141
  end
143
142
 
143
+ def self.view(opts, argv)
144
+ format = opts[:template] == "europass" ? "xml" : opts[:template]
145
+ template = File.join(File.dirname(__FILE__), "/../templates/resume.#{format}.erb")
146
+ puts File.read template
147
+ end
148
+
144
149
  def self.generate(opts, argv)
145
150
  format = opts[:to] == "europass" ? "xml" : opts[:to]
146
151
  output = opts[:output] || "resume-#{Date.today}.#{format}"
@@ -217,6 +217,35 @@ module Resme
217
217
  }
218
218
  end
219
219
 
220
+ def self.view_opts
221
+ opts = OptionParser.new do |o|
222
+ o.banner = "view --template FORMAT # Print template used for format"
223
+ o.on("-t", "--template FORMAT", String, "View template for FORMAT")
224
+ end
225
+
226
+ help = <<-EOS
227
+ NAME
228
+ #{opts.banner}
229
+
230
+ SYNOPSYS
231
+ #{opts.to_s}
232
+
233
+ DESCRIPTION
234
+ Print template used for FORMAT
235
+
236
+ EXAMPLES
237
+ resme view --template md
238
+ EOS
239
+
240
+ {
241
+ view: {
242
+ name: :view,
243
+ options: opts,
244
+ help: help.gsub(" ", "")
245
+ }
246
+ }
247
+ end
248
+
220
249
  def self.generate_opts
221
250
  opts = OptionParser.new do |o|
222
251
  o.banner = "generate [options] resume.yml ... # output resume"
@@ -0,0 +1,293 @@
1
+ require "classy_hash"
2
+ require "date"
3
+
4
+ module Resme
5
+ module ResumeStructureValidator
6
+ OPTIONAL_STRING = [:optional, String, NilClass]
7
+ OPTIONAL_PARTIAL_DATE = [:optional, Date, String, Integer, NilClass]
8
+ PARTIAL_DATE = [Date, String, Integer]
9
+
10
+ def self.validate(loaded_yaml)
11
+ errors = []
12
+ ClassyHash.validate(
13
+ loaded_yaml,
14
+ SCHEMA,
15
+ errors: errors,
16
+ strict: true,
17
+ raise_errors: true,
18
+ full: true
19
+ )
20
+ errors
21
+ end
22
+
23
+ #
24
+ # This defines the structure of resume.yml
25
+ # We validate it with ClassyHash
26
+ #
27
+ SCHEMA = {
28
+ "basics" => {
29
+ "first_name" => String,
30
+ "middle_name" => OPTIONAL_STRING,
31
+ "last_name" => String,
32
+ "title" => OPTIONAL_STRING,
33
+ "picture" => OPTIONAL_STRING,
34
+ "birthdate" => [:optional, Date, NilClass],
35
+ "nationality" => OPTIONAL_STRING,
36
+ "marital_status" => OPTIONAL_STRING,
37
+ "gender" => OPTIONAL_STRING
38
+ },
39
+ "contacts" => [[ {
40
+ "label" => String,
41
+ "value" => String
42
+ } ]],
43
+ "addresses" => [[ {
44
+ "label" => String,
45
+ "street" => OPTIONAL_STRING,
46
+ "zip_code" => [:optional, String, Integer, NilClass],
47
+ "city" => OPTIONAL_STRING,
48
+ "country" => OPTIONAL_STRING
49
+ } ]],
50
+ "web_presence" => [:optional,
51
+ [[
52
+ {
53
+ "label" => String,
54
+ "value" => String
55
+ },
56
+ ]],
57
+ NilClass
58
+ ],
59
+ "summary" => OPTIONAL_STRING,
60
+ "work" => [:optional,
61
+ [[
62
+ {
63
+ "who" => OPTIONAL_STRING,
64
+ "website" => OPTIONAL_STRING,
65
+ "address" => OPTIONAL_STRING,
66
+ "till" => OPTIONAL_PARTIAL_DATE,
67
+ "from" => OPTIONAL_PARTIAL_DATE,
68
+ "role" => String,
69
+ "summary" => String,
70
+ "details" => OPTIONAL_STRING
71
+ },
72
+ ]],
73
+ NilClass
74
+ ],
75
+ "teaching" => [:optional,
76
+ [[
77
+ {
78
+ "who" => String,
79
+ "school" => OPTIONAL_STRING,
80
+ "address" => OPTIONAL_STRING,
81
+ "till" => OPTIONAL_PARTIAL_DATE,
82
+ "from" => OPTIONAL_PARTIAL_DATE,
83
+ "role" => String,
84
+ "subject" => String,
85
+ "summary" => OPTIONAL_STRING,
86
+ "details" => OPTIONAL_STRING
87
+ }
88
+ ]],
89
+ NilClass
90
+ ],
91
+ "projects" => [:optional,
92
+ [[
93
+ {
94
+ "name" => String,
95
+ "size" => OPTIONAL_STRING,
96
+ "who" => OPTIONAL_STRING,
97
+ "till" => OPTIONAL_PARTIAL_DATE,
98
+ "from" => OPTIONAL_PARTIAL_DATE,
99
+ "role" => String,
100
+ "summary" => OPTIONAL_STRING,
101
+ }
102
+ ]],
103
+ NilClass
104
+ ],
105
+ "other" => [:optional,
106
+ [[
107
+ {
108
+ "who" => OPTIONAL_STRING,
109
+ "till" => OPTIONAL_PARTIAL_DATE,
110
+ "from" => OPTIONAL_PARTIAL_DATE,
111
+ "role" => String,
112
+ "summary" => OPTIONAL_STRING,
113
+ }
114
+ ]],
115
+ NilClass
116
+ ],
117
+ "committees" => [:optional,
118
+ [[
119
+ {
120
+ "who" => String,
121
+ "role" => String,
122
+ "editions" => [String, Integer],
123
+ "url" => OPTIONAL_STRING,
124
+ }
125
+ ]],
126
+ NilClass
127
+ ],
128
+ "volunteer" => [:optional,
129
+ [[
130
+ {
131
+ "who" => String,
132
+ "where" => OPTIONAL_STRING,
133
+ "date" => OPTIONAL_PARTIAL_DATE,
134
+ "till" => OPTIONAL_PARTIAL_DATE,
135
+ "from" => OPTIONAL_PARTIAL_DATE,
136
+ "role" => String,
137
+ "summary" => OPTIONAL_STRING,
138
+ }
139
+ ]],
140
+ NilClass
141
+ ],
142
+ "visits" => [:optional,
143
+ [[
144
+ {
145
+ "who" => String,
146
+ "address" => OPTIONAL_STRING,
147
+ "till" => OPTIONAL_PARTIAL_DATE,
148
+ "from" => OPTIONAL_PARTIAL_DATE,
149
+ "role" => String,
150
+ "summary" => OPTIONAL_STRING,
151
+ }
152
+ ]],
153
+ NilClass
154
+ ],
155
+ "education" => [:optional,
156
+ [[
157
+ {
158
+ "degree" => OPTIONAL_STRING,
159
+ "topic" => OPTIONAL_STRING,
160
+ "school" => String,
161
+ "address" => OPTIONAL_STRING,
162
+ "date" => OPTIONAL_PARTIAL_DATE,
163
+ "till" => OPTIONAL_PARTIAL_DATE,
164
+ "from" => OPTIONAL_PARTIAL_DATE,
165
+ "publish" => TrueClass,
166
+ "score" => [:optional, String, Integer, NilClass],
167
+ }
168
+ ]],
169
+ NilClass
170
+ ],
171
+ "publications" => [:optional,
172
+ [[
173
+ {
174
+ "title" => String,
175
+ "authors" => String,
176
+ "publisher" => String,
177
+ "date" => PARTIAL_DATE,
178
+ "url" => OPTIONAL_STRING,
179
+ }
180
+ ]],
181
+ NilClass
182
+ ],
183
+ "talks" => [:optional,
184
+ [[
185
+ {
186
+ "title" => String,
187
+ "venue" => String,
188
+ "date" => PARTIAL_DATE,
189
+ "url" => OPTIONAL_STRING,
190
+ }
191
+ ]],
192
+ NilClass
193
+ ],
194
+ "awards" => [:optional,
195
+ [[
196
+ {
197
+ "who" => String,
198
+ "address" => OPTIONAL_STRING,
199
+ "date" => PARTIAL_DATE,
200
+ "title" => String,
201
+ "summary" => OPTIONAL_STRING
202
+ }
203
+ ]],
204
+ NilClass
205
+ ],
206
+ "achievements" => [:optional,
207
+ [[
208
+ {
209
+ "who" => String,
210
+ "address" => OPTIONAL_STRING,
211
+ "date" => OPTIONAL_PARTIAL_DATE,
212
+ "title" => String,
213
+ "summary" => OPTIONAL_STRING
214
+ }
215
+ ]],
216
+ NilClass
217
+ ],
218
+ "software" => [:optional,
219
+ [[
220
+ {
221
+ "title" => String,
222
+ "url" => OPTIONAL_STRING,
223
+ "programming_language" => OPTIONAL_STRING,
224
+ "license" => OPTIONAL_STRING,
225
+ "role" => OPTIONAL_STRING,
226
+ "summary" => OPTIONAL_STRING,
227
+ }
228
+ ]],
229
+ NilClass
230
+ ],
231
+ "skills" => [:optional,
232
+ [[
233
+ {
234
+ "name" => String,
235
+ "level" => OPTIONAL_STRING,
236
+ "summary" => OPTIONAL_STRING,
237
+ }
238
+ ]],
239
+ NilClass
240
+ ],
241
+ "driving" => [:optional,
242
+ [[ { "license" => String, } ]],
243
+ NilClass
244
+ ],
245
+ "languages" => [:optional,
246
+ {
247
+ "mother_tongues" => [[
248
+ {
249
+ "code" => OPTIONAL_STRING,
250
+ "language" => String,
251
+ }
252
+ ]],
253
+ "foreign" => [:optional,
254
+ [[
255
+ {
256
+ "code" => OPTIONAL_STRING,
257
+ "language" => String,
258
+ "level" => OPTIONAL_STRING,
259
+ "listening" => OPTIONAL_STRING,
260
+ "reading" => OPTIONAL_STRING,
261
+ "spoken_interaction" => OPTIONAL_STRING,
262
+ "spoken_production" => OPTIONAL_STRING,
263
+ "writing" => OPTIONAL_STRING
264
+ }
265
+ ]],
266
+ NilClass
267
+ ]
268
+ },
269
+ NilClass
270
+ ],
271
+ "interests" => [:optional,
272
+ [[
273
+ {
274
+ "name" => String,
275
+ "level" => OPTIONAL_STRING,
276
+ "summary" => OPTIONAL_STRING,
277
+ }
278
+ ]],
279
+ NilClass
280
+ ],
281
+ "references" => [:optional,
282
+ [[
283
+ {
284
+ "name" => String,
285
+ "reference" => String,
286
+ "contacts" => [[ {"label" => String, "value" => String} ]]
287
+ }
288
+ ]],
289
+ NilClass
290
+ ]
291
+ }
292
+ end
293
+ end