blue_button_parser 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +22 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +114 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/blue_button_parser.gemspec +64 -0
- data/lib/blue_button_parser.rb +282 -0
- data/test/data/blue_button_example_data.txt +1479 -0
- data/test/data/expected_json_output.js +1062 -0
- data/test/helper.rb +19 -0
- data/test/test_blue_button_parser.rb +257 -0
- metadata +153 -0
data/.document
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
|
6
|
+
# Add dependencies to develop your gem here.
|
7
|
+
# Include everything needed to run rake, tests, features, etc.
|
8
|
+
group :development do
|
9
|
+
gem "shoulda", ">= 0"
|
10
|
+
gem "bundler", "~> 1.0.0"
|
11
|
+
gem "jeweler", "~> 1.6.4"
|
12
|
+
gem "rcov", ">= 0"
|
13
|
+
gem "json"
|
14
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
git (1.2.5)
|
5
|
+
jeweler (1.6.4)
|
6
|
+
bundler (~> 1.0)
|
7
|
+
git (>= 1.2.5)
|
8
|
+
rake
|
9
|
+
json (1.5.4)
|
10
|
+
rake (0.9.2.2)
|
11
|
+
rcov (0.9.11)
|
12
|
+
shoulda (2.11.3)
|
13
|
+
|
14
|
+
PLATFORMS
|
15
|
+
ruby
|
16
|
+
|
17
|
+
DEPENDENCIES
|
18
|
+
bundler (~> 1.0.0)
|
19
|
+
jeweler (~> 1.6.4)
|
20
|
+
json
|
21
|
+
rcov
|
22
|
+
shoulda
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 PatientsLikeMe
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
= BlueButtonParser
|
2
|
+
|
3
|
+
BlueButtonParser parses a BlueButton free-text personal health data file and translates it into a structured hash suitable for computational purposes.
|
4
|
+
|
5
|
+
BlueButton[http://www.va.gov/bluebutton/] is the initiative from the U.S. Department of Veterans Affairs
|
6
|
+
to allow veterans to download their information from the "My HealtheVet" personal health record
|
7
|
+
into a very simple text file. Because this file was meant to be human readable, not computationally readable, the file contains almost no markup or delimiters for sections, keys, or values.
|
8
|
+
|
9
|
+
BlueButtonParser was created by reverse-engineering the one
|
10
|
+
{sample data file provided by the VA}[http://www.va.gov/BLUEBUTTON/docs/VA_My_HealtheVet_Blue_Button_Sample_Version_12_All_Data.txt]
|
11
|
+
and creating some ad-hoc rules for how to parse the document. BlueButtonParser will attempt to find all the sections in the file, all the key-value pairs within that section, and even find collections of items within a section when applicable (e.g. the array of facilities in the section "TREATMENT FACILITIES").
|
12
|
+
|
13
|
+
<b>Example free text data</b>
|
14
|
+
|
15
|
+
----------------------------- DEMOGRAPHICS ----------------------------
|
16
|
+
|
17
|
+
Source: Self-Entered
|
18
|
+
|
19
|
+
First Name: ONE
|
20
|
+
Middle Initial: A
|
21
|
+
Last Name: MHVVETERAN
|
22
|
+
Suffix:
|
23
|
+
Alias: MHVVET
|
24
|
+
Relationship to VA: Patient, Veteran, Employee
|
25
|
+
|
26
|
+
Gender: Male Blood Type: AB+ Organ Donor: Yes
|
27
|
+
|
28
|
+
Date of Birth: 01 Mar 1948
|
29
|
+
Marital Status: Married
|
30
|
+
Current Occupation: Truck Driver
|
31
|
+
|
32
|
+
<b>Example parsed data (JSON)</b>
|
33
|
+
|
34
|
+
"DEMOGRAPHICS": {
|
35
|
+
"Source": "Self-Entered",
|
36
|
+
"First Name": "ONE",
|
37
|
+
"Middle Initial": "A",
|
38
|
+
"Last Name": "MHVVETERAN",
|
39
|
+
"Suffix": null,
|
40
|
+
"Alias": "MHVVET",
|
41
|
+
"Relationship to VA": "Patient, Veteran, Employee",
|
42
|
+
"Gender": "Male",
|
43
|
+
"Blood Type": "AB+",
|
44
|
+
"Organ Donor": "Yes",
|
45
|
+
"Date of Birth": "01 Mar 1948",
|
46
|
+
"Marital Status": "Married",
|
47
|
+
"Current Occupation": "Truck Driver"
|
48
|
+
}
|
49
|
+
|
50
|
+
== Install
|
51
|
+
|
52
|
+
sudo gem install blue_button_parser
|
53
|
+
|
54
|
+
== Usage
|
55
|
+
require 'blue_button_parser'
|
56
|
+
|
57
|
+
# parse your downloaded BlueButton data file
|
58
|
+
my_bb_file = File.read("test/data/blue_button_example_data.txt")
|
59
|
+
bbp = BlueButtonParser.new(my_bb_file)
|
60
|
+
|
61
|
+
# access the parsed data as a Hash
|
62
|
+
parsed_data_hash = bbp.data
|
63
|
+
|
64
|
+
# example: to see the list of sections parsed:
|
65
|
+
parsed_data_hash.keys
|
66
|
+
# => ["MY HEALTHEVET ACCOUNT SUMMARY", "VITALS AND READINGS", "HEALTH INSURANCE", ... ]
|
67
|
+
|
68
|
+
# example: to acccess the "MY HEALTHEVET ACCOUNT SUMMARY" section:
|
69
|
+
summary = parsed_data_hash["MY HEALTHEVET ACCOUNT SUMMARY"]
|
70
|
+
# => {"Authentication Facility Name"=>"SLC10 TEST LAB", "Authentication Date"=>"19 Aug 2010", ... }
|
71
|
+
|
72
|
+
= Caveats
|
73
|
+
|
74
|
+
BlueButtonParser was reverse engineered based on the single sample data file provided by the VA (latest version: v12, 02 Dec 2011). Because there are no rules as to how the document should be formatted,
|
75
|
+
|
76
|
+
* A later version of the BlueButton output could break the parser
|
77
|
+
* An instance of BlueButton output for a second patient might have slightly different formatting--for example, line-wrapped values for fields that were not wrapped in the current reference file, or new fields that were not applicable to the patient in the current reference file
|
78
|
+
* As new sections get added, there is no guarantee that they will conform to the formatting for other sections
|
79
|
+
|
80
|
+
To keep the BlueButtonParser up-to-date, the test data file (test/data/blue_button_example_data.txt) and expect parsed output (test/data/expected_json_output.js) should be updated every time a new version of the BlueButtonData file is released.
|
81
|
+
|
82
|
+
Note however that as far as I know, there is no formal notification that a new version of the sample data file has been released, so I guess developers will just need to be vigilant. :)
|
83
|
+
|
84
|
+
After updates, make sure the tests still work and any applicable new tests get added.
|
85
|
+
|
86
|
+
= Prior work
|
87
|
+
|
88
|
+
Somebody took a stab at this in the past:
|
89
|
+
http://rest-developer-edition.na8.force.com/BlueConverter
|
90
|
+
|
91
|
+
I believe the code for this example is found here:
|
92
|
+
https://github.com/joshbirk/BlueConverter
|
93
|
+
|
94
|
+
BlueConverter great first pass implementation, but it needs a few corrections:
|
95
|
+
|
96
|
+
* it doesn't deal with multiple key-values on single line, e.g. "Gender: Male Blood Type: AB+ Organ Donor: Yes"
|
97
|
+
* it doesn't deal with values that wrap lines
|
98
|
+
* it doesn't recognize that some of the key-values in a section should be grouped into a collection of items, e.g. in the "TREATMENT FACILITIES" section is actually comprised of a series of facilities
|
99
|
+
* it doesn't parse the tables, e.g. the "VA Treating Facility" in the "MY HEALTHEVET ACCOUNT SUMMARY"
|
100
|
+
|
101
|
+
== Contributing to BlueButtonParser
|
102
|
+
|
103
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
104
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
105
|
+
* Fork the project
|
106
|
+
* Start a feature/bugfix branch
|
107
|
+
* Commit and push until you are happy with your contribution
|
108
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
109
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
110
|
+
|
111
|
+
== Copyright
|
112
|
+
|
113
|
+
Copyright (c) 2012 PatientsLikeMe. See LICENSE.txt for further details.
|
114
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
+
gem.name = "blue_button_parser"
|
18
|
+
gem.homepage = "http://github.com/patientslikeme/blue_button_parser"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = %Q{Converts a BlueButton free text data file to a structured data Hash}
|
21
|
+
gem.description = %Q{Converts a BlueButton free text data file to a structured data Hash}
|
22
|
+
gem.email = "open_source@patientslikeme.com"
|
23
|
+
gem.authors = ["PatientsLikeMe"]
|
24
|
+
# dependencies defined in Gemfile
|
25
|
+
end
|
26
|
+
Jeweler::RubygemsDotOrgTasks.new
|
27
|
+
|
28
|
+
require 'rake/testtask'
|
29
|
+
Rake::TestTask.new(:test) do |test|
|
30
|
+
test.libs << 'lib' << 'test'
|
31
|
+
test.pattern = 'test/**/test_*.rb'
|
32
|
+
test.verbose = true
|
33
|
+
end
|
34
|
+
|
35
|
+
require 'rcov/rcovtask'
|
36
|
+
Rcov::RcovTask.new do |test|
|
37
|
+
test.libs << 'test'
|
38
|
+
test.pattern = 'test/**/test_*.rb'
|
39
|
+
test.verbose = true
|
40
|
+
test.rcov_opts << '--exclude "gems/*"'
|
41
|
+
end
|
42
|
+
|
43
|
+
task :default => :test
|
44
|
+
|
45
|
+
require 'rake/rdoctask'
|
46
|
+
Rake::RDocTask.new do |rdoc|
|
47
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
48
|
+
|
49
|
+
rdoc.rdoc_dir = 'rdoc'
|
50
|
+
rdoc.title = "blue_button_parser #{version}"
|
51
|
+
rdoc.rdoc_files.include('README*')
|
52
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
53
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{blue_button_parser}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["PatientsLikeMe"]
|
12
|
+
s.date = %q{2011-12-22}
|
13
|
+
s.description = %q{Converts a BlueButton free text data file to a structured data Hash}
|
14
|
+
s.email = %q{open_source@patientslikeme.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE.txt",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
"Gemfile",
|
22
|
+
"Gemfile.lock",
|
23
|
+
"LICENSE.txt",
|
24
|
+
"README.rdoc",
|
25
|
+
"Rakefile",
|
26
|
+
"VERSION",
|
27
|
+
"blue_button_parser.gemspec",
|
28
|
+
"lib/blue_button_parser.rb",
|
29
|
+
"test/data/blue_button_example_data.txt",
|
30
|
+
"test/data/expected_json_output.js",
|
31
|
+
"test/helper.rb",
|
32
|
+
"test/test_blue_button_parser.rb"
|
33
|
+
]
|
34
|
+
s.homepage = %q{http://github.com/patientslikeme/blue_button_parser}
|
35
|
+
s.licenses = ["MIT"]
|
36
|
+
s.require_paths = ["lib"]
|
37
|
+
s.rubygems_version = %q{1.4.1}
|
38
|
+
s.summary = %q{Converts a BlueButton free text data file to a structured data Hash}
|
39
|
+
|
40
|
+
if s.respond_to? :specification_version then
|
41
|
+
s.specification_version = 3
|
42
|
+
|
43
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
44
|
+
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
45
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
46
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
|
47
|
+
s.add_development_dependency(%q<rcov>, [">= 0"])
|
48
|
+
s.add_development_dependency(%q<json>, [">= 0"])
|
49
|
+
else
|
50
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
51
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
52
|
+
s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
|
53
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
54
|
+
s.add_dependency(%q<json>, [">= 0"])
|
55
|
+
end
|
56
|
+
else
|
57
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
58
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
59
|
+
s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
|
60
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
61
|
+
s.add_dependency(%q<json>, [">= 0"])
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
@@ -0,0 +1,282 @@
|
|
1
|
+
class BlueButtonParser
|
2
|
+
|
3
|
+
attr_reader :text, :data, :config
|
4
|
+
|
5
|
+
ALWAYS_SKIP_LINES = ["^[-]+$", "^[-]+[ ]+$", "^[ ]+[-]+", "^[=]+$", "^(- ){5,}", "END OF MY HEALTHEVET"]
|
6
|
+
|
7
|
+
DEFAULT_CONFIG = {
|
8
|
+
"MY HEALTHEVET PERSONAL INFORMATION REPORT" => {
|
9
|
+
:same_line_keys => ["Name", "Date of Birth"],
|
10
|
+
},
|
11
|
+
"DOWNLOAD REQUEST SUMMARY" => {},
|
12
|
+
"MY HEALTHEVET ACCOUNT SUMMARY" => {
|
13
|
+
:collection => {"Facilities" => {:table_columns => ["VA Treating Facility", " Type"]}}
|
14
|
+
},
|
15
|
+
"DEMOGRAPHICS" => {
|
16
|
+
:collection => {"EMERGENCY CONTACTS" => {:item_starts_with => "Contact First Name"}},
|
17
|
+
:same_line_keys => [["Gender", "Blood Type", "Organ Donor"], ["Work Phone Number", "Extension"]],
|
18
|
+
},
|
19
|
+
"HEALTH CARE PROVIDERS" => {
|
20
|
+
:collection => {"Providers" => {:item_starts_with => "Provider Name"}},
|
21
|
+
:same_line_keys => ["Phone Number", "Ext"],
|
22
|
+
},
|
23
|
+
"TREATMENT FACILITIES" => {
|
24
|
+
:collection => {"Facilities" => {:item_starts_with => "Facility Name"}},
|
25
|
+
:same_line_keys => [["Facility Type", "VA Home Facility"], ["Phone Number", "Ext"]],
|
26
|
+
},
|
27
|
+
"HEALTH INSURANCE" => {
|
28
|
+
:collection => {"Companies" => {:item_starts_with => "Health Insurance Company"}},
|
29
|
+
:same_line_keys => [["ID Number", "Group Number"], ["Start Date", "Stop Date"]],
|
30
|
+
},
|
31
|
+
"VA WELLNESS REMINDERS" => {
|
32
|
+
:collection => {"Reminders" => {:table_columns => ["Wellness Reminder", "Due Date", "Last Completed", "Location"]}}
|
33
|
+
},
|
34
|
+
"VA APPOINTMENTS" => {
|
35
|
+
:collection => {"Appointments" => {:item_starts_with => "Date/Time"}},
|
36
|
+
:skip_lines => ["^FUTURE APPOINTMENTS:", "^PAST APPOINTMENTS:"]
|
37
|
+
},
|
38
|
+
"VA MEDICATION HISTORY" => {
|
39
|
+
:collection => {"Medications" => {:item_starts_with => "Medication"}},
|
40
|
+
},
|
41
|
+
"MEDICATIONS AND SUPPLEMENTS" => {
|
42
|
+
:collection => {"Medications" => {:item_starts_with => "Category"}},
|
43
|
+
:same_line_keys => [["Start Date", "Stop Date"], ["Pharmacy Name", "Pharmacy Phone"]],
|
44
|
+
},
|
45
|
+
"VA ALLERGIES" => {
|
46
|
+
:collection => {"Allergies" => {:item_starts_with => "Allergy Name"}},
|
47
|
+
},
|
48
|
+
"ALLERGIES/ADVERSE REACTIONS" => {
|
49
|
+
:collection => {"Allergies" => {:item_starts_with => "Allergy Name"}},
|
50
|
+
},
|
51
|
+
"MEDICAL EVENTS" => {
|
52
|
+
:collection => {"Event" => {:item_starts_with => "Medical Event"}},
|
53
|
+
},
|
54
|
+
"IMMUNIZATIONS" => {
|
55
|
+
:collection => {"Immunizations" => {:item_starts_with => "Immunization"}},
|
56
|
+
},
|
57
|
+
"VA LABORATORY RESULTS" => {
|
58
|
+
:collection => {"Labs" => {:item_starts_with => "Lab Test"}},
|
59
|
+
},
|
60
|
+
"LABS AND TESTS" => {
|
61
|
+
:collection => {"Labs" => {:item_starts_with => "Test Name"}},
|
62
|
+
},
|
63
|
+
"VITALS AND READINGS" => {
|
64
|
+
:collection => {"Reading" => {:item_starts_with => "Measurement Type"}},
|
65
|
+
},
|
66
|
+
"FAMILY HEALTH HISTORY" => {
|
67
|
+
:collection => {"Relation" => {:item_starts_with => "Relationship"}},
|
68
|
+
},
|
69
|
+
"MILITARY HEALTH HISTORY" => {
|
70
|
+
:same_line_keys => [["Service Branch", "Rank"],["Location of Service", "Onboard Ship"]]
|
71
|
+
},
|
72
|
+
"DOD MILITARY SERVICE INFORMATION" => {
|
73
|
+
:collection => {
|
74
|
+
"Regular Active Service" => {:table_starts_with => "-- Regular Active Service", :table_columns => ["Service", "Begin Date", "End Date", "Character of Service", "Rank"] },
|
75
|
+
"Reserve/Guard Association Periods" => {:table_starts_with => "-- Reserve/Guard Association Periods", :table_columns => ["Service", "Begin Date", "End Date", "Character of Service", "Rank"] },
|
76
|
+
"DoD MOS/Occupation Codes" => {:table_starts_with => "-- Note: Both Service and DoD Generic codes", :table_columns => ["Service", "Begin Date", "Enl/Off", "Type", "Svc Occ Code", "DoD Occ Code"]}
|
77
|
+
},
|
78
|
+
:skip_lines => ["^Translations of Codes Used in this Section"]
|
79
|
+
}
|
80
|
+
}
|
81
|
+
|
82
|
+
def initialize(bb_data_text, config=DEFAULT_CONFIG, newline="\n")
|
83
|
+
@text = bb_data_text
|
84
|
+
@config= config
|
85
|
+
@data = parse_text(@text, newline)
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def new_section?(line)
|
91
|
+
new_section = line.match(/^[-]+ (.*) [-]+/)
|
92
|
+
new_section = new_section[1] if new_section
|
93
|
+
return new_section
|
94
|
+
end
|
95
|
+
|
96
|
+
def new_collection?(line, last_line, current_section)
|
97
|
+
new_collection = nil
|
98
|
+
|
99
|
+
if collections = sect_config(current_section, :collection)
|
100
|
+
collections.each_pair do |name, collection_config|
|
101
|
+
if starts_with = collection_config[:item_starts_with]
|
102
|
+
if line.match(Regexp.new("^#{starts_with}:"))
|
103
|
+
new_collection = name
|
104
|
+
break
|
105
|
+
end
|
106
|
+
elsif table_columns = collection_config[:table_columns]
|
107
|
+
new_table = false
|
108
|
+
|
109
|
+
if table_starts_with = collection_config[:table_starts_with]
|
110
|
+
if last_line.match(Regexp.new("^#{table_starts_with}"))
|
111
|
+
new_table = true
|
112
|
+
end
|
113
|
+
else
|
114
|
+
new_table = true
|
115
|
+
end
|
116
|
+
|
117
|
+
if new_table
|
118
|
+
regexp_str = ".?#{table_columns.join('.*')}.?"
|
119
|
+
if line.match(Regexp.new(regexp_str))
|
120
|
+
new_collection = name
|
121
|
+
break
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
return new_collection
|
130
|
+
end
|
131
|
+
|
132
|
+
def get_multi_key_values(line, current_section)
|
133
|
+
key_values = nil
|
134
|
+
|
135
|
+
if key_sets = sect_config(current_section, :same_line_keys)
|
136
|
+
unless key_sets.first.is_a?(Array)
|
137
|
+
key_sets = [key_sets]
|
138
|
+
end
|
139
|
+
|
140
|
+
key_sets.each do |keys|
|
141
|
+
regexp_str = keys.collect{|k| "#{k}: (.*)"}.join
|
142
|
+
regexp = Regexp.new(regexp_str)
|
143
|
+
if keys_match = line.match(regexp)
|
144
|
+
key_values = Hash.new
|
145
|
+
keys.each_with_index do |key, index|
|
146
|
+
key_values[key] = keys_match[index + 1]
|
147
|
+
end
|
148
|
+
break
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
return key_values
|
154
|
+
end
|
155
|
+
|
156
|
+
def get_single_key_value(line)
|
157
|
+
if key_match = line.match(/(.*)\: (.*)?/)
|
158
|
+
key_values = {key_match[1] => key_match[2]}
|
159
|
+
elsif key_match = line.match(/(.*):$/)
|
160
|
+
key_values = {key_match[1] => nil}
|
161
|
+
else
|
162
|
+
key_values = nil
|
163
|
+
end
|
164
|
+
|
165
|
+
return key_values
|
166
|
+
end
|
167
|
+
|
168
|
+
def get_key_values(line, current_section)
|
169
|
+
key_values = get_multi_key_values(line, current_section)
|
170
|
+
key_values = get_single_key_value(line) if key_values.nil?
|
171
|
+
return key_values
|
172
|
+
end
|
173
|
+
|
174
|
+
def key_ended?(line)
|
175
|
+
# either an empty line or a line starting with a key (e.g. "Status:") means we're done with multi-line
|
176
|
+
line.empty? or line.match(/^\S(.*): (\S*)/)
|
177
|
+
end
|
178
|
+
|
179
|
+
def sect_config(current_section, key)
|
180
|
+
if @config[current_section]
|
181
|
+
@config[current_section][key]
|
182
|
+
else
|
183
|
+
nil
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def parse_table_line(line, columns, column_widths)
|
188
|
+
row = Hash.new
|
189
|
+
columns.each_with_index do |column, index|
|
190
|
+
start = column_widths[index]
|
191
|
+
finish = if index == column_widths.size - 1
|
192
|
+
line.size
|
193
|
+
else
|
194
|
+
column_widths[index + 1]
|
195
|
+
end
|
196
|
+
val_str = (line[start, finish-start]).strip
|
197
|
+
val_str = val_str.empty? ? nil : val_str
|
198
|
+
row[column] = val_str
|
199
|
+
end
|
200
|
+
return row
|
201
|
+
end
|
202
|
+
|
203
|
+
def table_columns(current_section, current_collection)
|
204
|
+
sect_config(current_section, :collection)[current_collection][:table_columns]
|
205
|
+
end
|
206
|
+
|
207
|
+
def column_widths(line, columns)
|
208
|
+
columns.collect{|c| line.index(c)}
|
209
|
+
end
|
210
|
+
|
211
|
+
def parse_text(text, newline="\n")
|
212
|
+
# parse text line by line
|
213
|
+
lines = text.split(newline)
|
214
|
+
|
215
|
+
# state variables
|
216
|
+
current_section = nil
|
217
|
+
current_collection = nil
|
218
|
+
current_key = nil
|
219
|
+
current_table = nil
|
220
|
+
|
221
|
+
# put parsed data into this hash
|
222
|
+
data = Hash.new
|
223
|
+
|
224
|
+
# start parsing
|
225
|
+
lines.each_with_index do |line, index|
|
226
|
+
skip_regexps = (ALWAYS_SKIP_LINES + (sect_config(current_section, :skip_lines) || [])).compact
|
227
|
+
skip_regexps = skip_regexps.collect{|r| Regexp.new(r)}
|
228
|
+
next if skip_regexps.find{|re| re.match(line)}
|
229
|
+
|
230
|
+
if collection = new_collection?(line, lines[index - 1], current_section)
|
231
|
+
current_collection = collection
|
232
|
+
current_key = nil
|
233
|
+
data[current_section][current_collection] ||= []
|
234
|
+
if columns = table_columns(current_section, current_collection)
|
235
|
+
current_table = {:columns => columns, :widths => column_widths(line, columns)}
|
236
|
+
next
|
237
|
+
else
|
238
|
+
data[current_section][current_collection] << Hash.new
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
if key_ended?(line)
|
243
|
+
current_key = nil
|
244
|
+
end
|
245
|
+
|
246
|
+
if current_section and current_key
|
247
|
+
value = line.rstrip
|
248
|
+
if current_collection.nil?
|
249
|
+
data[current_section][current_key] = [data[current_section][current_key], value].compact.join(" \n")
|
250
|
+
else
|
251
|
+
(data[current_section][current_collection].last)[current_key] = [(data[current_section][current_collection].last)[current_key], value].compact.join(" \n")
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
if section = new_section?(line)
|
256
|
+
current_section = section
|
257
|
+
current_collection = nil
|
258
|
+
data[current_section] ||= {}
|
259
|
+
elsif current_table
|
260
|
+
if line.empty?
|
261
|
+
current_collection = nil
|
262
|
+
current_table = nil
|
263
|
+
else
|
264
|
+
data[current_section][current_collection] << parse_table_line(line, current_table[:columns], current_table[:widths])
|
265
|
+
end
|
266
|
+
elsif (!current_key and current_section and key_values = get_key_values(line, current_section))
|
267
|
+
key_values.each_pair do |key, value|
|
268
|
+
val = (value.nil? or value.strip.empty?) ? nil : value.strip #empty strings should be converted to nils
|
269
|
+
if current_collection.nil?
|
270
|
+
data[current_section][key] = val
|
271
|
+
else
|
272
|
+
(data[current_section][current_collection].last)[key] = val
|
273
|
+
end
|
274
|
+
current_key = key
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
return data
|
280
|
+
end
|
281
|
+
|
282
|
+
end
|