ncs_mdes 0.2.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,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