knartform 0.6.0

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.
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'json'
4
+ require 'pp'
5
+ require 'byebug'
6
+ require 'nokogiri'
7
+ require 'slim'
8
+
9
+ require_relative "../lib/common"
10
+ include Repository
11
+
12
+ HELP = <<EOF
13
+
14
+ Renders a number of validation metrics for every KNART XML document references in a provided manifest.json.
15
+
16
+ USAGE: #{__FILE__} <manifest.json> <output.html>
17
+
18
+ EXAMPLE:
19
+ #{__FILE__} manifest.json output.html
20
+
21
+ EOF
22
+
23
+ def report(good, bad)
24
+ puts "\nGood file references: #{good.length}"
25
+ puts "Bad file references: #{bad.length}"
26
+ bad.each do |n| puts "\t#{[n['path'],n['url']].join(' ')}" end
27
+ puts "\n"
28
+ end
29
+
30
+ def extract_metadata(doc)
31
+ return {
32
+ identifiers: doc.xpath("///xmlns:metadata/xmlns:identifiers/xmlns:identifier/@identifierName").count, #collect{|n| n.to_s},
33
+ relatedResources: doc.xpath("//xmlns:relatedResource//xmlns:resource").count,
34
+ supportingEvidence: doc.xpath("//xmlns:supportingEvidence//xmlns:resource").count,
35
+ applicability: doc.xpath("//xmlns:applicability/xmlns:coverage").count,
36
+ eventHistory: doc.xpath("//xmlns:eventHistory/xmlns:artifactLifeCycleEvent").count,
37
+ contributions: doc.xpath("//xmlns:contributions//xmlns:contributor").count,
38
+ publishers: doc.xpath("//xmlns:publishers//xmlns:publisher").count,
39
+ }
40
+ end
41
+
42
+ def knart_validations(doc)
43
+ # We'll cover the basics first.
44
+ report = {
45
+ doc: doc,
46
+ title: doc.xpath("//xmlns:title/@value").first.to_s,
47
+ artifactType: doc.xpath("//xmlns:artifactType/@value").first.to_s,
48
+ metadata: extract_metadata(doc)
49
+ }
50
+ # Global ELM stuff.
51
+ exps = doc.xpath("/xmlns:knowledgeDocument/xmlns:expressions/xmlns:def")
52
+
53
+ report[:expressions] = exps.collect{|e| {
54
+ name: e.xpath("@name").to_s,
55
+ type: e.xpath('elm:expression/@xsi:type').to_s
56
+ }}
57
+ # puts report
58
+ report
59
+ end
60
+
61
+ def composite_validations(doc)
62
+ report = {
63
+ doc: doc,
64
+ title: doc.xpath("//xmlns:title/@value").first.to_s,
65
+ artifactType: doc.xpath("//xmlns:artifactType/@value").first.to_s,
66
+ metadata: extract_metadata(doc),
67
+
68
+ }
69
+ # report[:artifacts] =
70
+ xml = doc.xpath('//xmlns:containedArtifacts').to_s
71
+ # byebug
72
+ # puts xml
73
+ report[:artifacts] = Hash.from_xml(xml)[:containedArtifacts][:artifact]
74
+ # puts report
75
+ report
76
+ end
77
+
78
+
79
+ def static_manifest_validations(root, manifest)
80
+ knarts = {}
81
+ composites = {}
82
+ manifest['groups'].each do |group|
83
+ group['items'].each do |item|
84
+ path = "#{root}/#{item['path']}"
85
+ # puts "#{path}"
86
+ if KNART_MIME_TYPES.include? item['mimeType']
87
+ doc = Nokogiri::XML(File.open(path))
88
+ begin
89
+ knarts[item['path']] = knart_validations(doc)
90
+ # puts knarts[item['path']][:metadata]
91
+ rescue
92
+ knarts[item['path']] = {
93
+ error: "Could not be validated"
94
+ }
95
+ end
96
+ elsif COMPOSITE_MIME_TYPES.include? item['mimeType']
97
+ doc = Nokogiri::XML(File.open(path))
98
+ begin
99
+ composites[item['path']] = composite_validations(doc)
100
+ # puts composites[item['path']][:metadata]
101
+ rescue
102
+ composites[item['path']] = {
103
+ error: "Could not be validated"
104
+ }
105
+ end
106
+ end
107
+ end
108
+ end
109
+ [knarts, composites]
110
+ end
111
+
112
+
113
+
114
+ def render_results(manifest, root, knarts, composites, out)
115
+ html = render_partial('spot_check.slim', {root: '..', manifest: manifest, knarts: knarts, composites: composites})
116
+ # html = Jade.compile File.read(template), locals: {results: results}
117
+ # puts html
118
+ file = File.open(out, 'w')
119
+ file.write html
120
+ file.close
121
+ end
122
+
123
+ if(ARGV.length != 2)
124
+ puts HELP
125
+ exit 1
126
+ else
127
+ file = ARGV[0]
128
+ manifest = JSON.parse(File.read(file))
129
+ root = File.dirname(File.expand_path(file))
130
+ knarts, composites = static_manifest_validations(root, manifest)
131
+ Slim::Engine.set_options pretty: true
132
+ render_results(manifest, root, knarts, composites, ARGV[1])
133
+ exit 0
134
+ end
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'json'
4
+ require 'pp'
5
+ require 'byebug'
6
+
7
+ require_relative '../lib/common'
8
+ include Repository
9
+ HELP = <<EOF.freeze
10
+
11
+ Updates an existing repository manifest.json file by merging in any additional files found in the root directory. If the existing and new manifest files are the same, the existing file will be *overwriten* in place. Be sure you have a backup handy if you're doing this!
12
+
13
+ USAGE: #{__FILE__} <content_root_directory> <existing_manifest.json> <new_manifest.json>
14
+
15
+ EXAMPLE:
16
+ #{__FILE__} . manifest.json manifest-new.json
17
+
18
+ EOF
19
+
20
+ def improveify_name!(item)
21
+ case item['path']
22
+ when /CCWP_/
23
+ item['name'] = 'Clinical Content White Paper'
24
+ when /HIMKWP_/
25
+ item['name'] = 'Harmonize and Integrate Member KNARTs White Paper'
26
+ when /ECA_/
27
+ item['name'] = 'ECA'
28
+ when /DT_/
29
+ item['name'] = 'Documentation Template'
30
+ when /OS_/
31
+ item['name'] = 'Order Set'
32
+ when /CRCK_/
33
+ item['name'] = 'Composite Artifact'
34
+ else
35
+ # Keep the existing name
36
+ end
37
+
38
+ case item['path']
39
+ when /KVRpt_/
40
+ item['name'] += ' Validation Report'
41
+ when /CSD_/
42
+ item['name'] += ' Conceptual Structure Document'
43
+ end
44
+ end
45
+
46
+ def existing_manifest_item_for(item, existing)
47
+ existing_item = nil
48
+ existing['groups'].each do |group, _i|
49
+ # puts "NAME: #{group['name']}"
50
+ # puts group
51
+ group['items'].each do |n|
52
+ if n['path'] == item['path']
53
+ existing_item = n
54
+ break
55
+ end
56
+ end
57
+ end
58
+ existing_item
59
+ end
60
+
61
+ def existing_manifest_group_for(name, existing)
62
+ existing_group = nil
63
+ existing['groups'].each do |group, _i|
64
+ # puts "#{name} #{group['name']}"
65
+ if group['name'].casecmp(name).zero?
66
+ existing_group = group
67
+ break
68
+ end
69
+ end
70
+ existing_group
71
+ end
72
+
73
+ # Forces keys to strings.
74
+ def rekey(hash)
75
+ hash.collect { |k, v| [k.to_s, v] }.to_h
76
+ end
77
+
78
+ # If the item path is already present *somewhere* in the manifest it will not be merged in, regardless of any differences in the generated and existing item summary. This is intentional to prevent manual changes from being overwritten. Merged in items will be grouped into a group name matching the item name; if an existing group cannot be identified, they will be put into a special placeholder group.
79
+ def merge_items_into(items, existing)
80
+ updated = []
81
+ placed = []
82
+ # placed_into_placeholder_group = []
83
+
84
+ items.each do |item|
85
+ placeholder_group = nil
86
+ if placeholder_group = existing_manifest_group_for(item['name'], existing)
87
+ # It already exists. Great.
88
+ elsif placeholder_group = existing_manifest_group_for(item['name'], existing)
89
+ # The placeholder group has already been created. Yay.
90
+ else
91
+ placeholder_group = rekey(
92
+ 'name': item['name'],
93
+ 'status': 'generated',
94
+ 'items': []
95
+ )
96
+ puts "Adding placeholder group to manifest: #{placeholder_group}"
97
+ existing['groups'] << placeholder_group
98
+ end
99
+
100
+ if existing_item = existing_manifest_item_for(item, existing)
101
+ # Updated security object only
102
+ # byebug
103
+ existing_item['security'] = item['security']
104
+ updated << existing_item
105
+ else
106
+ existing_group = existing_manifest_group_for(item['name'], existing)
107
+ # Just drop the item into the existing group.
108
+ improveify_name!(item)
109
+ # puts "#{item['name']} (#{item['path']})"
110
+ existing_group['items'] << item
111
+ placed << item
112
+ # else
113
+ # # No group in the existing manifest appears to be a fit, so put the item into a placeholder group.
114
+ # placeholder_group['items'] << item
115
+ # placed_into_placeholder_group << item
116
+ end
117
+ end
118
+ [updated, placed]
119
+ end
120
+
121
+ def report(updated, placed)
122
+ puts "\nFiles updated as already present in the manifest: #{updated.length}"
123
+ # updated.each do |n| puts "\t#{n['path']} (#{n['name']})" end
124
+ puts "Placed into a group: #{placed.length}"
125
+ placed.each { |n| puts "\t#{[n['path'], n['url']].join(' ')}" }
126
+ # puts "\nPut into a placeholder group:#{placed_into_placeholder_group.length}"
127
+ # placed_into_placeholder_group.each do |n| puts "\t#{n['path']}" end
128
+ end
129
+
130
+ def write_to_file(path, existing)
131
+ file = File.open(path, 'w')
132
+ file.write(JSON.pretty_generate(existing))
133
+ file.close
134
+ end
135
+
136
+ if ARGV.length != 3
137
+ puts HELP
138
+ exit 1
139
+ else
140
+ file = File.read(ARGV[1])
141
+ existing = JSON.parse(file)
142
+ items = content_directory_to_item_tree(ARGV[0])
143
+ updated, placed = merge_items_into(items, existing)
144
+ report(updated, placed)
145
+ write_to_file(ARGV[2], existing)
146
+ exit 0
147
+ end
@@ -0,0 +1,23 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'knartform'
3
+ s.version = '0.6.0'
4
+ s.summary = "HL7 CDS Knowledge Artifacts"
5
+ s.description = "Tools form working with HL7 CDS Knowledge Artifacts."
6
+ s.authors = ["Preston Lee"]
7
+ s.email = 'preston.lee@prestonlee.com'
8
+ s.homepage = 'https://github.com/preston/knartform'
9
+ s.license = 'MIT'
10
+
11
+ s.files = `git ls-files`.split("\n")
12
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
13
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
14
+ s.require_paths = ['lib']
15
+
16
+ s.add_dependency 'nokogiri'
17
+ s.add_dependency 'httparty'
18
+ s.add_dependency 'slim'
19
+
20
+ s.add_development_dependency 'bundler'
21
+ s.add_development_dependency 'rake'
22
+ s.add_development_dependency 'minitest'
23
+ end
@@ -0,0 +1,264 @@
1
+ # require 'xml/libxml'
2
+ require 'digest'
3
+
4
+ module Repository
5
+ SUPPORTED_MIME_TYPES = {
6
+ "application/pdf": {name: "PDF", expression: /\.pdf$/i},
7
+ "text/html": {name: "HTML", expression: /\.html?$/i},
8
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
9
+ {name: "Word", expression: /\.docx?$/i},
10
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
11
+ {name: "Excel", expression: /\.xlsx?$/i},
12
+ "application/hl7-cds-knowledge-artifact-1.3+xml": {name: 'HL7 KNART v1.3', expression: /KRprt(?!_CRCK).*\.xml$/i},
13
+ "application/docbook": {name: 'DocBook', expression: /(HIMKWP|KVRpt|CSD).*\.xml$/i},
14
+ 'application/hl7-cds-knowledge-artifact-composite+xml': {name: 'Composite Artifact', expression: /CRCK_.*\.xml$/i},
15
+ 'image/svg+xml': {name: "SVG Image", expression: /\.svg$/i}
16
+ }
17
+
18
+ KNART_MIME_TYPES = [
19
+ "application/hl7-cds-knowledge-artifact-1.3+xml"
20
+ ]
21
+
22
+ COMPOSITE_MIME_TYPES = [
23
+ 'application/hl7-cds-knowledge-artifact-composite+xml'
24
+ ]
25
+
26
+
27
+ DIGRAPH_TEMPLATE = <<TEMPLATE
28
+ digraph {
29
+ <% artifacts.each do |a| %>
30
+ artifact_<%= a[:hash] %>[label="<%= a[:name] %>\n<%= a[:file] %>", shape=rounded, style=filled, fillcolor=lightblue]<% end %>
31
+
32
+ <% events.each do |k, v| %>
33
+ event_<%= v %>[label="<%= k %>", style=filled, fillcolor=yellow]<% end %>
34
+
35
+ <% artifacts.each do |a| %><% a[:emits].each do |e| %>
36
+ artifact_<%= a[:hash] %> -> event_<%= events[e[:name]] %>[fontsize=8, label="emits\n<%= e[:conditions] %>"]<% end %><% end %>
37
+ <% artifacts.each do |a| %><% a[:triggers].each do |t| %>
38
+ event_<%= events[t] %> -> artifact_<%= a[:hash] %>[label="triggers"]<% end %><% end %>
39
+ }
40
+ TEMPLATE
41
+
42
+ def generate_dot(doc)
43
+ artifacts = []
44
+ events = {}
45
+ doc.xpath('//xmlns:containedArtifacts/xmlns:artifact').each do |a|
46
+ name = a.xpath('./xmlns:name/@value').to_s
47
+ file = a.xpath('./xmlns:reference/xmlns:url/@address').to_s
48
+ file = '(embedded)' if file.empty?
49
+ triggers = []
50
+ emits = []
51
+ tmp = {
52
+ name: name,
53
+ hash: Digest::SHA1.hexdigest(name),
54
+ file: file,
55
+ triggers: triggers,
56
+ emits: emits
57
+ }
58
+ artifacts << tmp
59
+
60
+ # Search for triggers
61
+ # puts a
62
+ a.xpath('.//xmlns:triggers/xmlns:trigger/@onEventName').each do |t|
63
+ value = t.to_s
64
+ triggers << value
65
+ events[value] = Digest::SHA1.hexdigest(value)
66
+ end
67
+
68
+ # Emmitted events within embedded KNARTs.
69
+ a.xpath('./xmlns:knowledgeDocument//xmlns:simpleAction[@xsi:type="FireEventAction"]').each do |action|
70
+ conditions = action.xpath('./xmlns:conditions').to_s.gsub('"', '\"')
71
+ # puts conditions
72
+ action.xpath('.//elm:element[@name="EventName"]/elm:value/@value').each do |n|
73
+ value = n.to_s
74
+ emits << {
75
+ name: value,
76
+ conditions: conditions
77
+ }
78
+ events[value] = Digest::SHA1.hexdigest(value)
79
+ end
80
+ end
81
+
82
+ # Emmitted events for referencesd KNARTs.
83
+ a.xpath('./xmlns:onCompletion//xmlns:eventName/@name').each do |n|
84
+ value = n.to_s
85
+ emits << {
86
+ name: value,
87
+ conditions: '(always)'
88
+ }
89
+ events[value] = Digest::SHA1.hexdigest(value)
90
+ end
91
+
92
+
93
+ end
94
+ # puts artifacts
95
+ # puts events
96
+ renderer = ERB.new(DIGRAPH_TEMPLATE)
97
+ renderer.result(binding)
98
+ end
99
+
100
+ # Forces keys to strings.
101
+ def rekey(hash)
102
+ hash.collect{|k,v| [k.to_s, v]}.to_h
103
+ end
104
+
105
+ def mimeTypeForFile(path)
106
+ mimeType = nil
107
+ SUPPORTED_MIME_TYPES.each do |k,v|
108
+ if v[:expression].match?(path)
109
+ mimeType = k
110
+ break
111
+ end
112
+ end
113
+ # puts mimeType || path
114
+ mimeType
115
+ end
116
+
117
+ def content_directory_to_item_tree(root)
118
+ content = []
119
+ list = Dir["#{root}/**/**"]
120
+ list.each do |n|
121
+ # puts n
122
+ if(File.file?(n))
123
+ # ext = File.extname(n).downcase[1..-1]
124
+ mimeType = mimeTypeForFile(n)
125
+ name = n.split('/')[1] #.split['.'][0]
126
+ name = name.split('_').collect(&:capitalize).join(' ')
127
+ tags = n.split('/').select{|d| d.length <= 4}
128
+ # Add subdirectory names to the tag list
129
+ subs = Dir.glob(File.dirname(n) + '/*').select{ |t|
130
+ puts t
131
+ # puts File.directory?(t)
132
+ # puts File.dirname(n) != t
133
+ # byebug
134
+ File.directory?(t) && n != t
135
+ }.collect{ |t| File.basename(t)}
136
+ puts "#{n} SUBS: #{subs}"
137
+ tags |= subs
138
+ if mimeType # it's a supported piece of content
139
+ item = {
140
+ 'name': name,
141
+ 'path': n,
142
+ 'mimeType': mimeType,
143
+ 'tags': tags
144
+ }
145
+ update_security(item)
146
+ item = rekey(item)
147
+ # Any directory name of 4 characters or less will be treated as a tag automatically
148
+ content << item
149
+ end
150
+ end
151
+ end
152
+ content
153
+ end
154
+
155
+ def update_security(item)
156
+ # byebug
157
+ content = File.read(item[:path])
158
+ item['security'] = rekey({
159
+ sha1: Digest::SHA1.hexdigest(content),
160
+ sha256: Digest::SHA256.hexdigest(content),
161
+ md5: Digest::MD5.hexdigest(content)
162
+ })
163
+ puts item
164
+ end
165
+
166
+ def audit_content_directory(root, manifest)
167
+ good = []
168
+ bad = []
169
+ manifest['groups'].each do |group|
170
+ group['items'].each do |item|
171
+ if item['path']
172
+ path = "#{root}/#{item['path']}"
173
+ if File.exist? path
174
+ good << item
175
+ else
176
+ bad << item
177
+ end
178
+ end
179
+ begin
180
+ if item['url']
181
+ response = HTTParty.head(item['url'])
182
+ if(response.code >= 400)
183
+ bad << item
184
+ else
185
+ good << item
186
+ end
187
+ end
188
+ rescue SocketError => e
189
+ # Probably a bad DNS or protocol name.
190
+ bad << item
191
+ end
192
+ end
193
+ end
194
+ return [good, bad]
195
+
196
+ end
197
+
198
+ def render_partial(name, stuff)
199
+ template = File.join(File.dirname(File.expand_path(__FILE__)), name)
200
+ Slim::Template.new(template).render(self, stuff)
201
+ end
202
+
203
+ end
204
+
205
+ # USAGE: Hash.from_xml(YOUR_XML_STRING)require 'rubygems'
206
+ require 'nokogiri'
207
+ # modified from http://stackoverflow.com/questions/1230741/convert-a-nokogiri-document-to-a-ruby-hash/1231297#1231297
208
+
209
+ class Hash
210
+ class << self
211
+ def from_xml(xml_io)
212
+ begin
213
+ result = Nokogiri::XML(xml_io)
214
+ return { result.root.name.to_sym => xml_node_to_hash(result.root)}
215
+ rescue Exception => e
216
+ # raise your custom exception here
217
+ end
218
+ end
219
+
220
+ def xml_node_to_hash(node)
221
+ # If we are at the root of the document, start the hash
222
+ if node.element?
223
+ result_hash = {}
224
+ if node.attributes != {}
225
+ attributes = {}
226
+ node.attributes.keys.each do |key|
227
+ attributes[node.attributes[key].name.to_sym] = node.attributes[key].value
228
+ end
229
+ end
230
+ if node.children.size > 0
231
+ node.children.each do |child|
232
+ result = xml_node_to_hash(child)
233
+
234
+ if child.name == "text"
235
+ unless child.next_sibling || child.previous_sibling
236
+ return result unless attributes
237
+ result_hash[child.name.to_sym] = result
238
+ end
239
+ elsif result_hash[child.name.to_sym]
240
+
241
+ if result_hash[child.name.to_sym].is_a?(Object::Array)
242
+ result_hash[child.name.to_sym] << result
243
+ else
244
+ result_hash[child.name.to_sym] = [result_hash[child.name.to_sym]] << result
245
+ end
246
+ else
247
+ result_hash[child.name.to_sym] = result
248
+ end
249
+ end
250
+ if attributes
251
+ #add code to remove non-data attributes e.g. xml schema, namespace here
252
+ #if there is a collision then node content supersets attributes
253
+ result_hash = attributes.merge(result_hash)
254
+ end
255
+ return result_hash
256
+ else
257
+ return attributes
258
+ end
259
+ else
260
+ return node.content.to_s
261
+ end
262
+ end
263
+ end
264
+ end