ncs_mdes 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,103 @@
1
+ require 'ncs_navigator/mdes'
2
+
3
+ require 'forwardable'
4
+
5
+ module NcsNavigator::Mdes
6
+ ##
7
+ # Implements the mechanism for determining where the MDES documents
8
+ # are stored on a particular system.
9
+ class SourceDocuments
10
+ BASE_ENV_VAR = 'NCS_MDES_DOCS_DIR'
11
+
12
+ extend Forwardable
13
+
14
+ ##
15
+ # The base path for all paths that are not explicitly
16
+ # configured. It defaults to `'documents'` within this gem and may
17
+ # be globally overridden by setting `NCS_MDES_DOCS_DIR` in the
18
+ # runtime environment.
19
+ #
20
+ # There's probably no reason to change this in the current version
21
+ # of the gem.
22
+ #
23
+ # @return [String]
24
+ attr_accessor :base
25
+
26
+ ##
27
+ # The MDES version this set of documents describes.
28
+ #
29
+ # @return [String]
30
+ attr_accessor :version
31
+
32
+ ##
33
+ # Instance-level alias for {.xmlns}.
34
+ # @method xmlns
35
+ # @return [Hash]
36
+ def_delegator self, :xmlns
37
+
38
+ class << self
39
+ ##
40
+ # Constructs an appropriate instance for the given version.
41
+ #
42
+ # @return [SourceDocuments]
43
+ def get(version)
44
+ case version
45
+ when '1.2'
46
+ create('1.2', '1.2/Data_Transmission_Schema_V1.2.xsd')
47
+ when '2.0'
48
+ create('2.0', '2.0/NCS_Transmission_Schema_2.0.01.02.xml')
49
+ else
50
+ raise "MDES #{version} is not supported by this version of ncs_mdes"
51
+ end
52
+ end
53
+
54
+ def create(version, schema)
55
+ self.new.tap do |sd|
56
+ sd.version = version
57
+ sd.schema = schema
58
+ end
59
+ end
60
+ private :create
61
+
62
+ ##
63
+ # A mapping of prefixes to XML namespaces for use with
64
+ # Nokogiri XPath.
65
+ #
66
+ # @return [Hash<String, String>]
67
+ def xmlns
68
+ {
69
+ 'xs' => 'http://www.w3.org/2001/XMLSchema',
70
+ 'ncs' => 'http://www.nationalchildrensstudy.gov',
71
+ 'ncsdoc' => 'http://www.nationalchildrensstudy.gov/doc'
72
+ }
73
+ end
74
+ end
75
+
76
+ def base
77
+ @base ||= (
78
+ ENV[BASE_ENV_VAR] ||
79
+ File.expand_path(File.join('..', '..', '..', '..', 'documents'), __FILE__)
80
+ )
81
+ end
82
+
83
+ ##
84
+ # The absolute path to the XML Schema describing the MDES
85
+ # transmission structure for this instance.
86
+ #
87
+ # @return [String]
88
+ def schema
89
+ @schema[0, 1] == '/' ? @schema : File.join(base, @schema)
90
+ end
91
+
92
+ ##
93
+ # Set the path to the MDES transmission structure XML Schema.
94
+ # If the path is relative (i.e., it does not begin with `/`), it
95
+ # will be interpreted relative to {#base}.
96
+ #
97
+ # @param [String] path
98
+ # @return [String] the provided path
99
+ def schema=(path)
100
+ @schema = path
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,86 @@
1
+ require 'ncs_navigator/mdes'
2
+
3
+ require 'forwardable'
4
+ require 'logger'
5
+ require 'nokogiri'
6
+
7
+ module NcsNavigator::Mdes
8
+ class Specification
9
+ extend Forwardable
10
+
11
+ ##
12
+ # @return [SourceDocuments] the source documents this reader is
13
+ # working from.
14
+ attr_accessor :source_documents
15
+
16
+ ##
17
+ # @method version
18
+ # @return [String] the version of the MDES to which this instance refers.
19
+ def_delegator :@source_documents, :version
20
+
21
+ ##
22
+ # @param [String,SourceDocuments] version either the string
23
+ # version of the MDES metadata you would like to read, or a
24
+ # {SourceDocuments} instance pointing to the appropriate files.
25
+ # @param [Hash] options
26
+ # @option options :log a logger to use while reading the specification. If
27
+ # not specified, a logger pointing to standard error will be used.
28
+ def initialize(version, options={})
29
+ @source_documents = case version
30
+ when SourceDocuments
31
+ version
32
+ else
33
+ SourceDocuments.get(version)
34
+ end
35
+ @log = options[:log] || NcsNavigator::Mdes.default_logger
36
+ end
37
+
38
+ ##
39
+ # @return [Nokogiri::XML::Document] the parsed version of the VDR
40
+ # XML schema for this version of the MDES.
41
+ def xsd
42
+ @xsd ||= Nokogiri::XML(File.read source_documents.schema)
43
+ end
44
+
45
+ ##
46
+ # @return [Array<TransmissionTable>] all the transmission tables
47
+ # in this version of the MDES.
48
+ def transmission_tables
49
+ @transmission_tables ||= read_transmission_tables
50
+ end
51
+
52
+ def read_transmission_tables
53
+ xsd.xpath(
54
+ '//xs:element[@name="transmission_tables"]/xs:complexType/xs:sequence/xs:element',
55
+ source_documents.xmlns
56
+ ).collect { |table_elt|
57
+ TransmissionTable.from_element(table_elt, :log => @log)
58
+ }.tap { |tables|
59
+ tables.each { |t| t.variables.each { |v| v.resolve_type!(types, :log => @log) } }
60
+ }
61
+ end
62
+ private :read_transmission_tables
63
+
64
+ ##
65
+ # @return [Array<VariableType>] all the named types in the
66
+ # MDES. This includes all the code lists.
67
+ def types
68
+ @types ||= read_types
69
+ end
70
+
71
+ def read_types
72
+ xsd.xpath('//xs:simpleType[@name]', source_documents.xmlns).collect do |type_elt|
73
+ VariableType.from_xsd_simple_type(type_elt, :log => @log)
74
+ end
75
+ end
76
+ private :read_types
77
+
78
+ ##
79
+ # A briefer inspection for nicer IRB sessions.
80
+ #
81
+ # @return [String]
82
+ def inspect
83
+ "#<#{self.class} version=#{version.inspect}>"
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,53 @@
1
+ require 'ncs_navigator/mdes'
2
+
3
+ module NcsNavigator::Mdes
4
+ ##
5
+ # One table in the MDES.
6
+ class TransmissionTable
7
+ ##
8
+ # Creates a new instance from an `xs:element` describing the table.
9
+ #
10
+ # @return [TransmissionTable] the created instance.
11
+ def self.from_element(element, options={})
12
+ log = options[:log] || NcsNavigator::Mdes.default_logger
13
+
14
+ new(element['name']).tap do |table|
15
+ table.variables = element.
16
+ xpath('xs:complexType/xs:sequence/xs:element', SourceDocuments.xmlns).
17
+ collect { |col_elt| Variable.from_element(col_elt, options) }
18
+ end
19
+ end
20
+
21
+ ##
22
+ # @return [String] the machine name of the table. This is also the name of the XML
23
+ # element in the VDR export.
24
+ attr_reader :name
25
+
26
+ ##
27
+ # @return [Array<Variable>] the variables that make up this
28
+ # table. (A relational model might call these the columns of this
29
+ # table.)
30
+ attr_accessor :variables
31
+
32
+ def initialize(name)
33
+ @name = name
34
+ end
35
+
36
+ ##
37
+ # Search for a variable by name.
38
+ #
39
+ # @param variable_name [String] the name of the variable to look for.
40
+ # @return [Variable] the variable with the given name, if any
41
+ def [](variable_name)
42
+ variables.find { |c| c.name == variable_name }
43
+ end
44
+
45
+ ##
46
+ # Provides a briefer inspection for cleaner IRB use.
47
+ #
48
+ # @return [String]
49
+ def inspect
50
+ "\#<#{self.class} name=#{name.inspect}>"
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,119 @@
1
+ require 'ncs_navigator/mdes'
2
+
3
+ module NcsNavigator::Mdes
4
+ ##
5
+ # A single field in the MDES. A relational model might also call
6
+ # this a column, but "variable" is what it's called in the MDES.
7
+ class Variable
8
+ ##
9
+ # @return [String] the name of the variable
10
+ attr_reader :name
11
+
12
+ ##
13
+ # @return [Boolean,:possible,:unknown,String] the PII category
14
+ # for the variable. `true` if it is definitely PII, `false` if
15
+ # it definitely is not, `:possible` if it was marked in the MDES
16
+ # as requiring manual review, `:unknown` if the MDES does not
17
+ # specify, or a string if the parsed value was not mappable to
18
+ # one of the above. Note that this value will always be truthy
19
+ # unless the MDES explicitly says that the variable is not PII.
20
+ attr_accessor :pii
21
+
22
+ ##
23
+ # @return [:active,:new,:modified,:retired,String] the status of
24
+ # the variable in this version of the MDES. A String is returned
25
+ # if the source value doesn't match any of the expected values
26
+ # in the MDES.
27
+ attr_accessor :status
28
+
29
+ ##
30
+ # @return [VariableType] the type of this variable.
31
+ attr_accessor :type
32
+
33
+ ##
34
+ # Is the variable mandatory for a valid submission?
35
+ #
36
+ # @return [Boolean]
37
+ attr_accessor :required
38
+ alias :required? :required
39
+
40
+ class << self
41
+ ##
42
+ # Examines the given parsed element and creates a new
43
+ # variable. The resulting variable has all the attributes set
44
+ # which can be set without reference to any other parts of the
45
+ # MDES outside of this one variable definition.
46
+ #
47
+ # @param [Nokogiri::Element] element the source xs:element
48
+ # @return [Variable] a new variable instance
49
+ def from_element(element, options={})
50
+ log = options[:log] || NcsNavigator::Mdes.default_logger
51
+
52
+ new(element['name']).tap do |var|
53
+ var.required = (element['nillable'] == 'false')
54
+ var.pii =
55
+ case element['pii']
56
+ when 'Y'; true;
57
+ when 'P'; :possible;
58
+ when nil; :unknown;
59
+ when ''; false;
60
+ else element['pii'];
61
+ end
62
+ var.status =
63
+ case element['status']
64
+ when '1'; :active;
65
+ when '2'; :new;
66
+ when '3'; :modified;
67
+ when '4'; :retired;
68
+ else element['status'];
69
+ end
70
+ var.type =
71
+ if element['type']
72
+ if element['type'] =~ /^xs:/
73
+ VariableType.xml_schema_type(element['type'].sub(/^xs:/, ''))
74
+ else
75
+ VariableType.reference(element['type'])
76
+ end
77
+ elsif element.elements.collect { |e| e.name } == %w(simpleType)
78
+ VariableType.from_xsd_simple_type(element.elements.first, options)
79
+ else
80
+ log.warn("Could not determine a type for variable #{var.name.inspect} on line #{element.line}")
81
+ nil
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ def initialize(name)
88
+ @name = name
89
+ end
90
+
91
+ def constraints
92
+ @constraints ||= []
93
+ end
94
+
95
+ ##
96
+ # If the {#type} of this instance is a reference to an NCS type,
97
+ # attempts to replace it with the full version from the given list
98
+ # of types.
99
+ #
100
+ # @param [Array<VariableType>] types
101
+ # @return [void]
102
+ def resolve_type!(types, options={})
103
+ log = options[:log] || NcsNavigator::Mdes.default_logger
104
+
105
+ return unless type && type.reference?
106
+ unless type.name =~ /^ncs:/
107
+ log.warn("Unknown reference namespace in type #{type.name.inspect} for #{name}")
108
+ end
109
+
110
+ ncs_type_name = type.name.sub(/^ncs:/, '')
111
+ match = types.find { |t| t.name == ncs_type_name }
112
+ if match
113
+ self.type = match
114
+ else
115
+ log.warn("Undefined type #{type.name} for #{name}.") if log
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,196 @@
1
+ require 'ncs_navigator/mdes'
2
+
3
+ module NcsNavigator::Mdes
4
+ ##
5
+ # Encapsulates restrictions on the content of a {Variable}.
6
+ class VariableType
7
+ attr_reader :name
8
+
9
+ ##
10
+ # @return [Symbol, nil] the XML Schema base type that this
11
+ # variable type is based on.
12
+ attr_accessor :base_type
13
+
14
+ ##
15
+ # @return [Regexp, nil] a regular expression that valid values of this
16
+ # type must match.
17
+ attr_accessor :pattern
18
+
19
+ ##
20
+ # @return [Fixnum, nil] the maximum length of a valid value of
21
+ # this type.
22
+ attr_accessor :max_length
23
+
24
+ ##
25
+ # @return [Fixnum, nil] the minimum length of a valid value of
26
+ # this type.
27
+ attr_accessor :min_length
28
+
29
+ ##
30
+ # @return [CodeList<CodeListEntry>, nil] the fixed list of values
31
+ # that are valid for this type.
32
+ attr_accessor :code_list
33
+
34
+ ##
35
+ # @return [Boolean] whether this is a fully fleshed-out type or
36
+ # just a reference. If it is a reference, all fields except for
37
+ # {#name} should be ignored.
38
+ attr_accessor :reference
39
+ alias :reference? :reference
40
+
41
+ class << self
42
+ ##
43
+ # @param [Nokogiri::XML::Element] st the `xs:simpleType` element
44
+ # from which to build the instance
45
+ # @param [Hash] options
46
+ # @option options [#warn] :log the logger to which to direct warnings
47
+ #
48
+ # @return [VariableType] a new instance based on the provided
49
+ # simple type.
50
+ def from_xsd_simple_type(st, options={})
51
+ log = options[:log] || NcsNavigator::Mdes.default_logger
52
+
53
+ restriction = st.xpath('xs:restriction[@base="xs:string"]', SourceDocuments.xmlns).first
54
+ unless restriction
55
+ log.warn "Unsupported restriction base in simpleType on line #{st.line}"
56
+ return
57
+ end
58
+
59
+ new(st['name']).tap do |vt|
60
+ vt.base_type = :string
61
+ restriction.elements.each do |elt|
62
+ case elt.name
63
+ when 'pattern'
64
+ p = elt['value']
65
+ vt.pattern =
66
+ begin
67
+ Regexp.new(p)
68
+ rescue RegexpError
69
+ log.warn("Uncompilable pattern #{p.inspect} in simpleType#{(' ' + vt.name.inspect) if vt.name} on line #{elt.line}")
70
+ nil
71
+ end
72
+ when 'maxLength'
73
+ vt.max_length = elt['value'].to_i
74
+ when 'minLength'
75
+ vt.min_length = elt['value'].to_i
76
+ when 'enumeration'
77
+ (vt.code_list ||= CodeList.new) << CodeListEntry.from_xsd_enumeration(elt)
78
+ if elt['desc'] =~ /\S/
79
+ if vt.code_list.description.nil?
80
+ vt.code_list.description = elt['desc']
81
+ elsif vt.code_list.description != elt['desc']
82
+ log.warn("Code list entry on line #{elt.line} unexpectedly has a different desc from the first entry")
83
+ end
84
+ end
85
+ else
86
+ log.warn "Unsupported restriction element #{elt.name.inspect} on line #{elt.line}"
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ ##
93
+ # Creates an instance that represents a reference with the given
94
+ # name.
95
+ #
96
+ # @return [VariableType] a new instance
97
+ def reference(name)
98
+ new(name).tap do |vt|
99
+ vt.reference = true
100
+ end
101
+ end
102
+
103
+ ##
104
+ # Creates an instance corresponding to the given XML Schema
105
+ # simple base type.
106
+ #
107
+ # @return [VariableType] a new instance
108
+ def xml_schema_type(type_name)
109
+ new.tap do |vt|
110
+ vt.base_type = type_name.to_sym
111
+ end
112
+ end
113
+ end
114
+
115
+ def initialize(name=nil)
116
+ @name = name
117
+ end
118
+
119
+ def inspect
120
+ attrs = [
121
+ [:name, name.inspect],
122
+ [:base_type, base_type.inspect],
123
+ [:reference, reference.inspect],
124
+ [:code_list, code_list ? "<#{code_list.size} entries>" : nil]
125
+ ].reject { |k, v| v.nil? }.
126
+ collect { |k, v| "#{k}=#{v}" }
127
+ "#<#{self.class} #{attrs.join(' ')}>"
128
+ end
129
+
130
+ ##
131
+ # A specialization of `Array` for code lists.
132
+ #
133
+ # @see VariableType#code_list
134
+ # @see CodeListEntry
135
+ class CodeList < Array
136
+ ##
137
+ # @return [String,nil] the description of the code list if any.
138
+ attr_accessor :description
139
+ end
140
+
141
+ ##
142
+ # A single entry in a code list.
143
+ #
144
+ # @see VariableType#code_list
145
+ # @see CodeList
146
+ class CodeListEntry
147
+ ##
148
+ # @return [String] the local code value for the entry.
149
+ attr_reader :value
150
+
151
+ ##
152
+ # @return [String] the human-readable label for the entry.
153
+ attr_accessor :label
154
+
155
+ ##
156
+ # @return [String] the MDES's globally-unique identifier for
157
+ # this coded value.
158
+ attr_accessor :global_value
159
+
160
+ ##
161
+ # @return [String] the name of MDES's master code list from
162
+ # which this value is derived.
163
+ attr_accessor :master_cl
164
+
165
+ class << self
166
+ ##
167
+ # Creates a new instance from a `xs:enumeration` simple type
168
+ # restriction subelement.
169
+ #
170
+ # @param [Nokogiri::XML::Element] enum the `xs:enumeration`
171
+ # element.
172
+ # @param [Hash] options
173
+ # @option options [#warn] :log the logger to which to direct warnings
174
+ #
175
+ # @return [CodeListEntry]
176
+ def from_xsd_enumeration(enum, options={})
177
+ log = options[:log] || NcsNavigator::Mdes.default_logger
178
+
179
+ log.warn("Missing value for code list entry on line #{enum.line}") unless enum['value']
180
+
181
+ new(enum['value']).tap do |cle|
182
+ cle.label = enum['label']
183
+ cle.global_value = enum['global_value']
184
+ cle.master_cl = enum['master_cl']
185
+ end
186
+ end
187
+ end
188
+
189
+ def initialize(value)
190
+ @value = value
191
+ end
192
+
193
+ alias :to_s :value
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,5 @@
1
+ module NcsNavigator
2
+ module Mdes
3
+ VERSION = "0.2.0"
4
+ end
5
+ end
@@ -0,0 +1,27 @@
1
+ require 'logger'
2
+
3
+ module NcsNavigator
4
+ module Mdes
5
+ autoload :VERSION, 'ncs_navigator/mdes/version'
6
+
7
+ autoload :SourceDocuments, 'ncs_navigator/mdes/source_documents'
8
+ autoload :Specification, 'ncs_navigator/mdes/specification'
9
+ autoload :TransmissionTable, 'ncs_navigator/mdes/transmission_table'
10
+ autoload :Variable, 'ncs_navigator/mdes/variable'
11
+ autoload :VariableType, 'ncs_navigator/mdes/variable_type'
12
+
13
+ ##
14
+ # @return the default logger for this module when no other one is
15
+ # specified. It logs to standard error.
16
+ def self.default_logger
17
+ @default_logger ||= Logger.new($stderr)
18
+ end
19
+ end
20
+
21
+ ##
22
+ # @return [Mdes::Specification] a new {Mdes::Specification} for the given
23
+ # version.
24
+ def self.Mdes(version)
25
+ Mdes::Specification.new(version)
26
+ end
27
+ end
data/ncs_mdes.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "ncs_navigator/mdes/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "ncs_mdes"
7
+ s.version = NcsNavigator::Mdes::VERSION
8
+ s.authors = ["Rhett Sutphin"]
9
+ s.email = ["r-sutphin@northwestern.edu"]
10
+ s.homepage = ""
11
+ s.summary = %q{A ruby API for various versions of the NCS MDES.}
12
+ s.description = %q{
13
+ Provides a consistent ruby interface to the project metainformation in the
14
+ National Children's Study's Master Data Element Specification.
15
+ }
16
+
17
+ s.files = `git ls-files`.split("\n") - ['irb']
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_dependency 'nokogiri', '~> 1.4'
23
+
24
+ s.add_development_dependency 'rspec', '~> 2.6'
25
+ s.add_development_dependency 'rake', '~> 0.9.2'
26
+ s.add_development_dependency 'yard', '~> 0.7.2'
27
+ end