meibo 0.1.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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +13 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +84 -0
  6. data/Gemfile +16 -0
  7. data/Gemfile.lock +84 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +41 -0
  10. data/Rakefile +12 -0
  11. data/lib/meibo/academic_session.rb +48 -0
  12. data/lib/meibo/academic_session_set.rb +15 -0
  13. data/lib/meibo/base_profile.rb +37 -0
  14. data/lib/meibo/builder/academic_session_builder.rb +26 -0
  15. data/lib/meibo/builder/base_builder.rb +13 -0
  16. data/lib/meibo/builder/classroom_builder.rb +38 -0
  17. data/lib/meibo/builder/course_builder.rb +32 -0
  18. data/lib/meibo/builder/demographic_builder.rb +22 -0
  19. data/lib/meibo/builder/enrollment_builder.rb +30 -0
  20. data/lib/meibo/builder/organization_builder.rb +34 -0
  21. data/lib/meibo/builder/role_builder.rb +30 -0
  22. data/lib/meibo/builder/user_builder.rb +36 -0
  23. data/lib/meibo/builder/user_profile_builder.rb +22 -0
  24. data/lib/meibo/builder.rb +60 -0
  25. data/lib/meibo/classroom.rb +67 -0
  26. data/lib/meibo/classroom_set.rb +29 -0
  27. data/lib/meibo/converter.rb +259 -0
  28. data/lib/meibo/course.rb +45 -0
  29. data/lib/meibo/course_set.rb +23 -0
  30. data/lib/meibo/data_model.rb +84 -0
  31. data/lib/meibo/data_set.rb +44 -0
  32. data/lib/meibo/demographic.rb +70 -0
  33. data/lib/meibo/demographic_set.rb +18 -0
  34. data/lib/meibo/enrollment.rb +51 -0
  35. data/lib/meibo/enrollment_set.rb +22 -0
  36. data/lib/meibo/errors.rb +13 -0
  37. data/lib/meibo/factory_bot/academic_session.rb +23 -0
  38. data/lib/meibo/factory_bot/all.rb +13 -0
  39. data/lib/meibo/factory_bot/classroom.rb +28 -0
  40. data/lib/meibo/factory_bot/course.rb +21 -0
  41. data/lib/meibo/factory_bot/demographic.rb +16 -0
  42. data/lib/meibo/factory_bot/enrollment.rb +37 -0
  43. data/lib/meibo/factory_bot/manifest.rb +33 -0
  44. data/lib/meibo/factory_bot/memory_package.rb +11 -0
  45. data/lib/meibo/factory_bot/organization.rb +47 -0
  46. data/lib/meibo/factory_bot/role.rb +41 -0
  47. data/lib/meibo/factory_bot/user.rb +22 -0
  48. data/lib/meibo/factory_bot/user_profile.rb +21 -0
  49. data/lib/meibo/japan_profile/academic_session.rb +26 -0
  50. data/lib/meibo/japan_profile/classroom.rb +21 -0
  51. data/lib/meibo/japan_profile/course.rb +18 -0
  52. data/lib/meibo/japan_profile/demographic.rb +15 -0
  53. data/lib/meibo/japan_profile/enrollment.rb +35 -0
  54. data/lib/meibo/japan_profile/organization.rb +20 -0
  55. data/lib/meibo/japan_profile/role.rb +26 -0
  56. data/lib/meibo/japan_profile/user.rb +27 -0
  57. data/lib/meibo/japan_profile/user_profile.rb +7 -0
  58. data/lib/meibo/japan_profile.rb +39 -0
  59. data/lib/meibo/manifest/processing_mode.rb +40 -0
  60. data/lib/meibo/manifest.rb +163 -0
  61. data/lib/meibo/organization.rb +44 -0
  62. data/lib/meibo/organization_set.rb +15 -0
  63. data/lib/meibo/reader.rb +138 -0
  64. data/lib/meibo/role.rb +65 -0
  65. data/lib/meibo/role_set.rb +25 -0
  66. data/lib/meibo/roster.rb +166 -0
  67. data/lib/meibo/user.rb +71 -0
  68. data/lib/meibo/user_profile.rb +42 -0
  69. data/lib/meibo/user_profile_set.rb +18 -0
  70. data/lib/meibo/user_set.rb +24 -0
  71. data/lib/meibo/version.rb +5 -0
  72. data/lib/meibo.rb +17 -0
  73. data/meibo.gemspec +38 -0
  74. data/sig/meibo.rbs +4 -0
  75. metadata +148 -0
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Meibo
6
+ class Builder
7
+ module UserProfileBuilder
8
+ extend BaseBuilder
9
+
10
+ def self.builder_attribute_names
11
+ [:builder, :user]
12
+ end
13
+
14
+ def initialize(builder:, sourced_id: SecureRandom.uuid, user:, **kw)
15
+ super(sourced_id: sourced_id, user_sourced_id: user.sourced_id, **kw)
16
+ @builder = builder
17
+ @user = user
18
+ builder.user_profiles << self
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module Meibo
6
+ class Builder
7
+ extend Forwardable
8
+
9
+ attr_reader :package, :profile
10
+
11
+ def_delegators :@package, :academic_sessions, :classes, :courses, :demographics, :enrollments, :organizations, :roles, :users, :user_profiles
12
+
13
+ def initialize(package:, profile: BaseProfile)
14
+ @package = package
15
+ @profile = profile
16
+ end
17
+
18
+ def build_academic_session(**kw)
19
+ builder_for(:academic_session).new(builder: self, **kw)
20
+ end
21
+
22
+ def build_classroom(**kw)
23
+ builder_for(:class).new(builder: self, **kw)
24
+ end
25
+
26
+ def build_course(**kw)
27
+ builder_for(:course).new(builder: self, **kw)
28
+ end
29
+
30
+ def build_demographic(**kw)
31
+ builder_for(:demographic).new(builder: self, **kw)
32
+ end
33
+
34
+ def build_enrollment(**kw)
35
+ builder_for(:enrollment).new(builder: self, **kw)
36
+ end
37
+
38
+ def build_organization(**kw)
39
+ builder_for(:org).new(builder: self, **kw)
40
+ end
41
+
42
+ def build_role(**kw)
43
+ builder_for(:role).new(builder: self, **kw)
44
+ end
45
+
46
+ def build_user(**kw)
47
+ builder_for(:user).new(builder: self, **kw)
48
+ end
49
+
50
+ def build_user_profile(**kw)
51
+ builder_for(:user_profile).new(builder: self, **kw)
52
+ end
53
+
54
+ private
55
+
56
+ def builder_for(key)
57
+ profile.builder_for(key)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Meibo
4
+ class Classroom
5
+ TYPES = {
6
+ homeroom: 'homeroom',
7
+ scheduled: 'scheduled'
8
+ }.freeze
9
+
10
+ DataModel.define(
11
+ self,
12
+ attribute_name_to_header_field_map: {
13
+ sourced_id: 'sourcedId',
14
+ status: 'status',
15
+ date_last_modified: 'dateLastModified',
16
+ title: 'title',
17
+ grades: 'grades',
18
+ course_sourced_id: 'courseSourcedId',
19
+ class_code: 'classCode',
20
+ class_type: 'classType',
21
+ location: 'location',
22
+ school_sourced_id: 'schoolSourcedId',
23
+ term_sourced_ids: 'termSourcedIds',
24
+ subjects: 'subjects',
25
+ subject_codes: 'subjectCodes',
26
+ periods: 'periods'
27
+ }.freeze,
28
+ converters: {
29
+ datetime: [:date_last_modified].freeze,
30
+ enum: {
31
+ class_type: [*TYPES.values, ENUM_EXT_PATTERN].freeze
32
+ }.freeze,
33
+ list: [
34
+ :grades,
35
+ :term_sourced_ids,
36
+ :subjects,
37
+ :subject_codes,
38
+ :periods
39
+ ].freeze,
40
+ required: [:sourced_id, :title, :class_type, :course_sourced_id, :term_sourced_ids, :school_sourced_id].freeze,
41
+ status: [:status].freeze
42
+ }
43
+ )
44
+
45
+ def initialize(sourced_id:, status: nil, date_last_modified: nil, title:, grades: [], course_sourced_id:, class_code: nil, class_type:, location: nil, school_sourced_id:, term_sourced_ids:, subjects: [], subject_codes: [], periods: [], **extension_fields)
46
+ unless subjects.is_a?(Array) && subject_codes.is_a?(Array) && subjects.size == subject_codes.size
47
+ raise InvalidDataTypeError
48
+ end
49
+
50
+ @sourced_id = sourced_id
51
+ @status = status
52
+ @date_last_modified = date_last_modified
53
+ @title = title
54
+ @grades = grades
55
+ @course_sourced_id = course_sourced_id
56
+ @class_code = class_code
57
+ @class_type = class_type
58
+ @location = location
59
+ @school_sourced_id = school_sourced_id
60
+ @term_sourced_ids = term_sourced_ids
61
+ @subjects = subjects
62
+ @subject_codes = subject_codes
63
+ @periods = periods
64
+ @extension_fields = extension_fields
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Meibo
4
+ class ClassroomSet < DataSet
5
+ def initialize(data, academic_session_set:, course_set:, organization_set:)
6
+ super(data)
7
+ @academic_session_set = academic_session_set
8
+ @course_set = course_set
9
+ @organization_set = organization_set
10
+ end
11
+
12
+ def check_semantically_consistent
13
+ super
14
+
15
+ each do |classroom|
16
+ @organization_set.find_by_sourced_id(classroom.school_sourced_id)
17
+ @course_set.find_by_sourced_id(classroom.course_sourced_id)
18
+
19
+ if classroom.term_sourced_ids.empty?
20
+ raise DataNotFoundError, "termSourcedIdは1つ以上指定してください"
21
+ end
22
+
23
+ classroom.term_sourced_ids.each do |term_sourced_id|
24
+ @academic_session_set.find_by_sourced_id(term_sourced_id)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require 'time'
5
+
6
+ module Meibo
7
+ module Converter
8
+ TYPES = %i[
9
+ list
10
+ required
11
+ boolean
12
+ date
13
+ datetime
14
+ enum
15
+ integer
16
+ status
17
+ user_ids
18
+ year
19
+ ].freeze
20
+
21
+ class << self
22
+ def build_header_field_to_attribute_converter(attribute_name_to_header_field_map)
23
+ header_field_to_attribute_name_map = attribute_name_to_header_field_map.to_h {|attribute, header_field|
24
+ [header_field, attribute]
25
+ }.freeze
26
+ lambda {|field| header_field_to_attribute_name_map.fetch(field) }
27
+ end
28
+
29
+ def build_parser_converter(fields:, converters:)
30
+ build_converter(fields: fields, converters: converters, write_or_parser: 'parser')
31
+ end
32
+
33
+ def build_write_converter(fields:, converters:)
34
+ build_converter(fields: fields, converters: converters, write_or_parser: 'write')
35
+ end
36
+
37
+ private
38
+
39
+ def build_converter(fields:, converters:, write_or_parser:)
40
+ converter_list = TYPES.filter_map do |converter_type|
41
+ fields_to_be_converted = converters[converter_type]
42
+ method_name = "build_#{converter_type}_field_#{write_or_parser}_converter"
43
+ if fields_to_be_converted && respond_to?(method_name, true)
44
+ if converter_type == :enum
45
+ enum_definition = fields_to_be_converted.to_h {|field, enum| [fields.index(field), enum] }
46
+ send(method_name, enum_definition)
47
+ else
48
+ indexes = fields_to_be_converted.map {|field| fields.index(field) }
49
+ send(method_name, indexes)
50
+ end
51
+ end
52
+ end
53
+ lambda do |field, field_info|
54
+ # NOTE: convert blank sourcedId to nil
55
+ if field_info.index.zero?
56
+ field = nil if field.empty?
57
+ end
58
+ converter_list.each {|converter| field = converter[field, field_info] }
59
+ field
60
+ end
61
+ end
62
+
63
+ def build_boolean_field_parser_converter(boolean_field_indexes)
64
+ boolean_field_indexes = boolean_field_indexes.dup.freeze
65
+ lambda do |field, field_info|
66
+ if boolean_field_indexes.include?(field_info.index)
67
+ case field
68
+ when 'true'
69
+ true
70
+ when 'false'
71
+ false
72
+ when nil
73
+ nil
74
+ else
75
+ raise InvalidDataTypeError
76
+ end
77
+ else
78
+ field
79
+ end
80
+ end
81
+ end
82
+
83
+ def build_date_field_write_converter(date_field_indexes)
84
+ date_field_indexes = date_field_indexes.dup.freeze
85
+ lambda do |field, field_info|
86
+ if date_field_indexes.include?(field_info.index)
87
+ field&.iso8601
88
+ else
89
+ field
90
+ end
91
+ end
92
+ end
93
+
94
+ def build_date_field_parser_converter(date_field_indexes)
95
+ date_field_indexes = date_field_indexes.dup.freeze
96
+ lambda do |field, field_info|
97
+ if field && date_field_indexes.include?(field_info.index)
98
+ begin
99
+ Date.strptime(field, '%Y-%m-%d')
100
+ rescue
101
+ raise InvalidDataTypeError
102
+ end
103
+ else
104
+ field
105
+ end
106
+ end
107
+ end
108
+
109
+ def build_datetime_field_write_converter(datetime_field_indexes)
110
+ datetime_field_indexes = datetime_field_indexes.dup.freeze
111
+ lambda do |field, field_info|
112
+ if datetime_field_indexes.include?(field_info.index)
113
+ field&.utc&.iso8601
114
+ else
115
+ field
116
+ end
117
+ end
118
+ end
119
+
120
+ def build_datetime_field_parser_converter(datetime_field_indexes)
121
+ datetime_field_indexes = datetime_field_indexes.dup.freeze
122
+ lambda do |field, field_info|
123
+ if field && datetime_field_indexes.include?(field_info.index)
124
+ begin
125
+ Time.iso8601(field)
126
+ rescue
127
+ raise InvalidDataTypeError
128
+ end
129
+ else
130
+ field
131
+ end
132
+ end
133
+ end
134
+
135
+ def build_enum_field_parser_converter(enum_definition)
136
+ enum_definition = enum_definition.dup.freeze
137
+ lambda do |field, field_info|
138
+ return field unless field
139
+
140
+ enum = enum_definition[field_info.index]
141
+ if enum
142
+ unless enum.any? {|e| e.match?(field) }
143
+ raise InvalidDataTypeError
144
+ end
145
+ end
146
+ field
147
+ end
148
+ end
149
+
150
+ def build_integer_field_parser_converter(integer_field_indexes)
151
+ integer_field_indexes = integer_field_indexes.dup.freeze
152
+ lambda do |field, field_info|
153
+ if field && integer_field_indexes.include?(field_info.index)
154
+ begin
155
+ Integer(field, 10)
156
+ rescue
157
+ raise InvalidDataTypeError
158
+ end
159
+ else
160
+ field
161
+ end
162
+ end
163
+ end
164
+
165
+ def build_list_field_write_converter(list_field_indexes)
166
+ list_field_indexes = list_field_indexes.dup.freeze
167
+ lambda do |field, field_info|
168
+ if list_field_indexes.include?(field_info.index)
169
+ if field
170
+ if field.empty?
171
+ nil
172
+ else
173
+ field.join(',')
174
+ end
175
+ end
176
+ else
177
+ field
178
+ end
179
+ end
180
+ end
181
+
182
+ def build_list_field_parser_converter(list_field_indexes)
183
+ list_field_indexes = list_field_indexes.dup.freeze
184
+ lambda do |field, field_info|
185
+ if list_field_indexes.include?(field_info.index)
186
+ if field
187
+ field.split(',').map(&:strip)
188
+ else
189
+ []
190
+ end
191
+ else
192
+ field
193
+ end
194
+ end
195
+ end
196
+
197
+ def build_required_field_parser_converter(required_field_indexes)
198
+ required_field_indexes = required_field_indexes.dup.freeze
199
+ lambda do |field, field_info|
200
+ if required_field_indexes.include?(field_info.index)
201
+ raise MissingDataError if field.nil?
202
+ raise MissingDataError if field.respond_to?(:empty?) && field.empty?
203
+ end
204
+ field
205
+ end
206
+ end
207
+
208
+ # TODO
209
+ # def build_role_field_write_converter(role_field_indexes)
210
+
211
+ def build_status_field_parser_converter(status_field_indexes)
212
+ status_field_indexes = status_field_indexes.dup.freeze
213
+ lambda do |field, field_info|
214
+ if field && status_field_indexes.include?(field_info.index)
215
+ raise InvalidDataTypeError, "invalid status: #{field}" unless field == 'active' || field == 'tobedeleted'
216
+ else
217
+ field
218
+ end
219
+ end
220
+ end
221
+
222
+ def build_user_ids_field_parser_converter(user_ids_field_indexes)
223
+ user_ids_field_indexes = user_ids_field_indexes.dup.freeze
224
+ lambda do |field, field_info|
225
+ if user_ids_field_indexes.include?(field_info.index)
226
+ raise InvalidDataTypeError unless field.all? {|user_id| Meibo::User::USER_ID_FORMAT_REGEXP.match?(user_id) }
227
+ end
228
+ field
229
+ end
230
+ end
231
+
232
+ def build_year_field_write_converter(year_field_indexes)
233
+ year_field_indexes = year_field_indexes.dup.freeze
234
+ lambda do |field, field_info|
235
+ if year_field_indexes.include?(field_info.index)
236
+ field && ("%04d" % field)
237
+ else
238
+ field
239
+ end
240
+ end
241
+ end
242
+
243
+ def build_year_field_parser_converter(year_field_indexes)
244
+ year_field_indexes = year_field_indexes.dup.freeze
245
+ lambda do |field, field_info|
246
+ if field && year_field_indexes.include?(field_info.index)
247
+ begin
248
+ Integer(field, 10)
249
+ rescue
250
+ raise InvalidDataTypeError
251
+ end
252
+ else
253
+ field
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Meibo
4
+ class Course
5
+ DataModel.define(
6
+ self,
7
+ attribute_name_to_header_field_map: {
8
+ sourced_id: 'sourcedId',
9
+ status: 'status',
10
+ date_last_modified: 'dateLastModified',
11
+ school_year_sourced_id: 'schoolYearSourcedId',
12
+ title: 'title',
13
+ course_code: 'courseCode',
14
+ grades: 'grades',
15
+ org_sourced_id: 'orgSourcedId',
16
+ subjects: 'subjects',
17
+ subject_codes: 'subjectCodes'
18
+ },
19
+ converters: {
20
+ datetime: [:date_last_modified],
21
+ list: [:grades, :subjects, :subject_codes],
22
+ required: [:sourced_id, :title, :org_sourced_id],
23
+ status: [:status]
24
+ }
25
+ )
26
+
27
+ def initialize(sourced_id:, status: nil, date_last_modified: nil, school_year_sourced_id: nil, title:, course_code: nil, grades: [], org_sourced_id:, subjects: [], subject_codes: [], **extension_fields)
28
+ unless subjects.is_a?(Array) && subject_codes.is_a?(Array) && subjects.size == subject_codes.size
29
+ raise InvalidDataTypeError
30
+ end
31
+
32
+ @sourced_id = sourced_id
33
+ @status = status
34
+ @date_last_modified = date_last_modified
35
+ @school_year_sourced_id = school_year_sourced_id
36
+ @title = title
37
+ @course_code = course_code
38
+ @grades = grades
39
+ @org_sourced_id = org_sourced_id
40
+ @subjects = subjects
41
+ @subject_codes = subject_codes
42
+ @extension_fields = extension_fields
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Meibo
4
+ class CourseSet < DataSet
5
+ def initialize(data, academic_session_set:, organization_set:)
6
+ super(data)
7
+ @academic_session_set = academic_session_set
8
+ @organization_set = organization_set
9
+ end
10
+
11
+ def check_semantically_consistent
12
+ super
13
+
14
+ each do |course|
15
+ if course.school_year_sourced_id
16
+ @academic_session_set.find_by_sourced_id(course.school_year_sourced_id)
17
+ end
18
+
19
+ @organization_set.find_by_sourced_id(course.org_sourced_id)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'csv'
4
+
5
+ module Meibo
6
+ module DataModel
7
+ module ClassMethods
8
+ def parse(csv)
9
+ return to_enum(:parse, csv) unless block_given?
10
+
11
+ actual_headers = CSV.parse_line(csv)
12
+ missing_headers = header_fields - actual_headers
13
+ unless missing_headers.empty?
14
+ raise MissingHeadersError, "missing headers: #{missing_headers.join(',')}"
15
+ end
16
+ unless actual_headers.take(header_fields.size) == header_fields
17
+ raise ScrambledHeadersError
18
+ end
19
+
20
+ CSV.parse(csv, encoding: Meibo::CSV_ENCODING, headers: true, converters: parser_converters, header_converters: header_converters).each do |row|
21
+ yield new(**row.to_h)
22
+ end
23
+ end
24
+ end
25
+
26
+ def self.define(klass, attribute_name_to_header_field_map:, converters: {})
27
+ attribute_name_to_header_field_map = attribute_name_to_header_field_map.dup.freeze
28
+ attribute_names = attribute_name_to_header_field_map.keys.freeze
29
+ header_fields = attribute_name_to_header_field_map.values.freeze
30
+ converters = converters.dup.freeze
31
+ define_class_attribute(klass, :attribute_name_to_header_field_map, attribute_name_to_header_field_map)
32
+ define_class_attribute(klass, :attribute_names, attribute_names)
33
+ define_class_attribute(klass, :header_fields, header_fields)
34
+ define_class_attribute(klass, :converters, converters)
35
+
36
+ define_header_converters(klass, attribute_name_to_header_field_map)
37
+ define_parser_converters(klass, attribute_names: attribute_names, converters: converters)
38
+ define_write_converters(klass, attribute_names: attribute_names, converters: converters)
39
+
40
+ klass.attr_reader(*attribute_names, :extension_fields)
41
+ klass.extend(ClassMethods)
42
+ klass.include(self)
43
+ end
44
+
45
+ def self.define_class_attribute(klass, attribute, value)
46
+ klass.define_singleton_method(attribute) { value }
47
+ end
48
+
49
+ def self.define_header_converters(klass, attribute_name_to_header_field_map)
50
+ header_converters = Converter.build_header_field_to_attribute_converter(attribute_name_to_header_field_map)
51
+ klass.define_singleton_method(:header_converters) { header_converters }
52
+ end
53
+
54
+ def self.define_parser_converters(klass, attribute_names:, converters:)
55
+ parser_converter = Converter.build_parser_converter(fields: attribute_names, converters: converters)
56
+ klass.define_singleton_method(:parser_converters) { parser_converter }
57
+ end
58
+
59
+ def self.define_write_converters(klass, attribute_names:, converters:)
60
+ write_converter = Converter.build_write_converter(fields: attribute_names, converters: converters)
61
+ klass.define_singleton_method(:write_converters) { write_converter }
62
+ end
63
+
64
+ def to_csv(...)
65
+ to_a.to_csv(...)
66
+ end
67
+
68
+ def deconstruct
69
+ to_a
70
+ end
71
+
72
+ def deconstruct_keys(_keys)
73
+ to_h
74
+ end
75
+
76
+ def to_a
77
+ self.class.attribute_names.map {|attribute| public_send(attribute) }
78
+ end
79
+
80
+ def to_h
81
+ self.class.attribute_names.zip(to_a).to_h
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,44 @@
1
+ module Meibo
2
+ class DataSet
3
+ def initialize(data)
4
+ @data = data
5
+ @data_hash = data.to_h {|datum| [datum.sourced_id, datum] }
6
+ end
7
+
8
+ def <<(new_data)
9
+ raise DataNotFoundError, "sourcedIdがありません" unless new_data.sourced_id
10
+ raise SourcedIdDuplicatedError, 'sourcedIdが重複しています' if @data_hash.key?(new_data.sourced_id)
11
+
12
+ @data << new_data
13
+ @data_hash[new_data.sourced_id] = new_data
14
+ end
15
+
16
+ def check_semantically_consistent
17
+ unless @data.size == @data_hash.size
18
+ raise SourcedIdDuplicatedError, 'sourcedIdが重複しています'
19
+ end
20
+
21
+ unless @data_hash[nil].nil?
22
+ raise DataNotFoundError, "sourcedIdがありません"
23
+ end
24
+ end
25
+
26
+ def each(...)
27
+ @data.each(...)
28
+ end
29
+
30
+ def empty?
31
+ @data.empty?
32
+ end
33
+
34
+ def find_by_sourced_id(sourced_id)
35
+ @data_hash.fetch(sourced_id)
36
+ rescue KeyError
37
+ raise DataNotFoundError, "sourcedId: #{sourced_id} が見つかりません"
38
+ end
39
+
40
+ def to_a
41
+ @data
42
+ end
43
+ end
44
+ end