maimai_net 0.0.1

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,202 @@
1
+ module MaimaiNet::Model
2
+ module GenericComparison
3
+ refine Array do
4
+ def generic_of?(variants)
5
+ each_with_index.all? do |val, i| variants[i % variants.size] === val end
6
+ end
7
+
8
+ def map_class
9
+ empty? ? self.class : Generic[self.class, Either[*map(&:class)]]
10
+ end
11
+ end
12
+
13
+ refine Hash do
14
+ def generic_of?(variants)
15
+ [keys, values].each_with_index.all? do |li, i|
16
+ li.all? do |val| variants[i % variants.size] === val end
17
+ end
18
+ end
19
+
20
+ def map_class
21
+ empty? ?
22
+ self.class :
23
+ Generic[
24
+ self.class,
25
+ *([keys, values].map do |li| Either[*li.map(&:class)] end),
26
+ ]
27
+ end
28
+ end
29
+ end
30
+
31
+ using GenericComparison
32
+
33
+ module Variant
34
+ @class_method = Module.new do
35
+ def [](*args)
36
+ fail ArgumentError, 'no variants given' if args.empty?
37
+ fail ArgumentError, 'variants must be a module or class' if args.any? do |var|
38
+ !(Module === var) && !(Variant === var)
39
+ end
40
+
41
+ super
42
+ end
43
+ end
44
+
45
+ @class_method.singleton_class.class_exec do
46
+ def to_s
47
+ [Variant.to_s, 'InternalLookupFailsafe'].join('::')
48
+ end
49
+ alias inspect to_s
50
+ end
51
+
52
+ def self.included(cls)
53
+ super
54
+ class_method = @class_method
55
+ cls.singleton_class.prepend @class_method
56
+ cls.define_singleton_method :inherited do |subcls|
57
+ super(subcls)
58
+ subcls.singleton_class.prepend class_method
59
+ end if Class === cls
60
+ end
61
+
62
+ freeze
63
+ end
64
+
65
+ # @!api private
66
+ # defines a generic typing
67
+ class Generic
68
+ include Variant
69
+ include MaimaiNet::ModuleExt::MethodCache
70
+
71
+ def initialize(cls, variants)
72
+ @class = cls
73
+ @variants = variants.freeze
74
+ freeze
75
+ end
76
+
77
+ cache_method :hash do
78
+ [@class, *@variants].inject(0) do |hsh, type|
79
+ ((hsh >> 11) | type.hash) % (1 << (0.size << 3))
80
+ end
81
+ end
82
+
83
+ def ===(obj)
84
+ class_match = @class === obj
85
+ fail NotImplementedError, "#{obj.class} does not support generic" unless obj.respond_to?(:generic_of?)
86
+ internal_match = obj.generic_of?(variants)
87
+
88
+ class_match && internal_match
89
+ end
90
+
91
+ def to_s
92
+ "%s[%s]" % [
93
+ to_class.name,
94
+ variants.join(', '),
95
+ ]
96
+ end
97
+ alias inspect to_s
98
+
99
+ def to_class
100
+ @class
101
+ end
102
+ def variants
103
+ @variants
104
+ end
105
+
106
+ class << self
107
+ private :new
108
+
109
+ # defines a generic class statement
110
+ # @return [Generic]
111
+ def [](cls, *variants)
112
+ @_list ||= {}
113
+ gen = @_list.fetch(cls, []).find do |gen_| gen_.variants.eql?(variants) end
114
+ return gen unless gen.nil?
115
+
116
+ gen = new(cls, variants)
117
+ (@_list[cls] ||= []) << gen
118
+ gen
119
+ end
120
+ end
121
+ end
122
+
123
+ class Either
124
+ include Variant
125
+ include MaimaiNet::ModuleExt::MethodCache
126
+
127
+ def initialize(variants)
128
+ @variants = variants.freeze
129
+ freeze
130
+ end
131
+
132
+ cache_method :hash do
133
+ [*@variants].inject(0) do |hsh, type|
134
+ ((hsh >> 11) | type.hash) % (1 << (0.size << 3))
135
+ end
136
+ end
137
+
138
+ def ===(obj)
139
+ variants.any? do |variant| variant === obj end
140
+ end
141
+
142
+ def to_s
143
+ prefix = nil
144
+ prefix = 'Optional' if variants.include? NilClass
145
+
146
+ "%s(%s)" % [
147
+ prefix,
148
+ clean_variants.join('|'),
149
+ ]
150
+ end
151
+ alias inspect to_s
152
+
153
+ def variants
154
+ @variants
155
+ end
156
+
157
+ cache_method :clean_variants do
158
+ variants.reject do |variant| NilClass.eql? variant end
159
+ end
160
+
161
+ class << self
162
+ private :new
163
+
164
+ # defines an either class statement
165
+ # @return [Either]
166
+ def [](*variants)
167
+ variants.uniq!
168
+ return variants.first if variants.size == 1
169
+
170
+ @_list ||= {}
171
+ sorted_hash = variants.sort_by(&:object_id).hash
172
+ gen = @_list.fetch(sorted_hash, nil)
173
+ return gen unless gen.nil?
174
+
175
+ gen = new(variants)
176
+ @_list[sorted_hash] ||= gen
177
+ gen
178
+ end
179
+ end
180
+ end
181
+
182
+ module Optional
183
+ include Variant
184
+
185
+ class << self
186
+ def [](*variants)
187
+ variants << NilClass
188
+ variants.uniq!
189
+ fail ArgumentError, "invoking #{self.name}[#{variants.join(', ')}] is not allowed" if variants.size == 1
190
+
191
+ Either[*variants]
192
+ end
193
+
194
+ def append_features(cls); end
195
+ def prepend_features(cls); end
196
+ end
197
+ end
198
+
199
+ Boolean = Either[TrueClass, FalseClass]
200
+
201
+ private_constant :GenericComparison
202
+ end
@@ -0,0 +1,359 @@
1
+ module MaimaiNet
2
+ # data model used for parsed data from MaimaiNet::Page
3
+ module Model
4
+ require 'maimai_net/model-typing'
5
+
6
+ module Base
7
+ class Struct < ::Struct
8
+ using GenericComparison
9
+ # @param kwargs [Hash] options are strong-typed based on class definition
10
+ def initialize(**kwargs)
11
+ props = self.class.instance_variable_get(:@_properties)
12
+ keys = props.keys
13
+ optional_keys = props.select do |k, pr|
14
+ Either === pr[:class] &&
15
+ pr[:class].variants.include?(NilClass)
16
+ end.keys
17
+
18
+ missing_keys = keys - (kwargs.keys | optional_keys)
19
+ fail KeyError, "#{missing_keys.join(', ')} is not defined for #{self.class}" unless missing_keys.empty?
20
+ kwargs.each do |key, value|
21
+ fail KeyError, "#{key} is not defined as struct member" unless keys.include?(key)
22
+ class_str = value.respond_to?(:map_class) ? value.map_class : value.class
23
+ fail TypeError, "#{key} type mismatch, given #{class_str}, expected #{props[key][:class]}" unless props[key][:class] === value
24
+ end
25
+
26
+ args = kwargs.values_at(*keys)
27
+ super(*args)
28
+ end
29
+ end
30
+ class << Struct
31
+ # creates a strong-typed struct data
32
+ # @param opts [Hash{Symbol => Module}]
33
+ # list of struct members along with respective type definition
34
+ # @return [Struct] new subclass with defined types
35
+ def new(**opts, &block)
36
+ super(*opts.keys) do
37
+ @_properties = {}
38
+ opts.each do |key, typedata|
39
+ @_properties[key] = case typedata
40
+ when Array
41
+ {class: Generic[*typedata]}
42
+ when Module, Variant
43
+ {class: typedata}
44
+ else
45
+ fail TypeError, "invalid type definition"
46
+ end
47
+ end
48
+
49
+ class_exec(&block) if block_given?
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ SongCount = Base::Struct.new(achieved: Integer, total: Integer) do
56
+ def to_s
57
+ "#{achieved}/#{total}"
58
+ end
59
+ alias inspect to_s
60
+ end
61
+
62
+ module PlayerCommon
63
+ Info = Base::Struct.new(
64
+ name: String,
65
+ title: String,
66
+ grade: String,
67
+ )
68
+ end
69
+
70
+ module PlayerData
71
+ Decoration = Base::Struct.new(
72
+ icon: URI::Generic,
73
+ )
74
+ ExtendedInfo = Base::Struct.new(
75
+ rating: Integer,
76
+ class_grade: String,
77
+ partner_star_total: Integer,
78
+ )
79
+
80
+ DifficultyStatistic = Base::Struct.new(
81
+ clears: SongCount,
82
+ ranks: Generic[Hash, Symbol, SongCount],
83
+ dx_ranks: Generic[Hash, Integer, SongCount],
84
+ flags: Generic[Hash, Symbol, SongCount],
85
+ sync_flags: Generic[Hash, Symbol, SongCount],
86
+ )
87
+
88
+ InfoPlate = Base::Struct.new(
89
+ info: PlayerCommon::Info,
90
+ decoration: Decoration,
91
+ extended: ExtendedInfo,
92
+ )
93
+ Lite = Base::Struct.new(
94
+ name: String,
95
+ rating: Integer,
96
+ )
97
+ Data = Base::Struct.new(
98
+ plate: InfoPlate,
99
+ statistics: Generic[Hash, Symbol, DifficultyStatistic],
100
+ )
101
+ end
102
+
103
+ WebID = Base::Struct.new(
104
+ item_hash: String,
105
+ item_key: String,
106
+ ) do
107
+ def self.parse(s)
108
+ hash, key = s[0, 128].b, s[128, s.size - 128].unpack1('m*').unpack1('H*')
109
+ new(item_hash: -hash, item_key: -key)
110
+ end
111
+
112
+ def to_str
113
+ self.item_hash + [[self.item_key].pack('H*')].pack('m0')
114
+ end
115
+ alias to_s to_str
116
+ end
117
+
118
+ class WebID
119
+ DUMMY_ID = -('0' * 128 + 'A' * 44)
120
+ DUMMY = parse(DUMMY_ID)
121
+ def DUMMY.inspect
122
+ '#<%s %s>' % [
123
+ self.class,
124
+ -'dummy',
125
+ ]
126
+ end
127
+ end
128
+
129
+ module Chart
130
+ info_base = {
131
+ title: String,
132
+ type: String,
133
+ difficulty: Integer,
134
+ }
135
+
136
+ InfoLite = Base::Struct.new(**info_base) do
137
+ def to_info(level_text: '?')
138
+ Info.new(
139
+ web_id: WebID::DUMMY,
140
+ title: title,
141
+ type: type,
142
+ difficulty: difficulty,
143
+ level_text: level_text,
144
+ )
145
+ end
146
+ end
147
+
148
+ Info = Base::Struct.new(
149
+ web_id: WebID,
150
+ **info_base,
151
+ level_text: String,
152
+ ) do
153
+ def to_lite
154
+ InfoLite.new(title: title, type: type, difficulty: difficulty)
155
+ end
156
+ end
157
+
158
+ Song = Base::Struct.new(
159
+ title: String,
160
+ artist: String,
161
+ genre: String,
162
+ jacket: URI::Generic,
163
+ )
164
+ end
165
+
166
+ SongEntry = Base::Struct.new(
167
+ web_id: WebID,
168
+ title: String,
169
+ genre: String,
170
+ )
171
+
172
+ SongFavoriteInfo = Base::Struct.new(
173
+ song: SongEntry,
174
+ flag: Boolean,
175
+ )
176
+
177
+ PhotoUpload = Base::Struct.new(
178
+ info: Chart::InfoLite,
179
+ url: URI::Generic,
180
+ location: String,
181
+ time: Time,
182
+ )
183
+
184
+ module Result
185
+ Progress = Base::Struct.new(
186
+ value: Integer,
187
+ max: Integer,
188
+ ) do
189
+ def to_s; "#{value}/#{max}"; end
190
+ alias to_i value
191
+ alias inspect to_s
192
+ end
193
+
194
+ RivalInfo = Base::Struct.new(
195
+ player: PlayerData::Lite,
196
+ score: Float,
197
+ )
198
+
199
+ TourMember = Base::Struct.new(
200
+ icon: URI::Generic,
201
+ grade: Integer,
202
+ level: Integer,
203
+ )
204
+
205
+ Judgment = Base::Struct.new(
206
+ just: Integer,
207
+ perfect: Integer,
208
+ great: Integer,
209
+ good: Integer,
210
+ miss: Integer,
211
+ )
212
+
213
+ Offset = Base::Struct.new(
214
+ early: Integer,
215
+ late: Integer,
216
+ )
217
+
218
+ Challenge = Base::Struct.new(
219
+ type: Symbol,
220
+ lives: Progress,
221
+ )
222
+
223
+ ScoreLite = Base::Struct.new(
224
+ score: Float,
225
+ deluxe_score: Progress,
226
+ grade: Symbol,
227
+ flags: Generic[Array, Symbol],
228
+ )
229
+
230
+ Score = Base::Struct.new(
231
+ score: Float,
232
+ deluxe_score: Progress,
233
+ combo: Progress,
234
+ sync_score: Progress,
235
+ grade: Symbol,
236
+ flags: Generic[Array, Symbol],
237
+ )
238
+
239
+ ReferenceWebID = Base::Struct.new(
240
+ order: Integer,
241
+ time: Time,
242
+ ) do
243
+ def self.parse(s)
244
+ order, time = s.split(',').first(2).map(&:to_i)
245
+ new(order: order, time: Time.at(time).localtime(32400).freeze)
246
+ end
247
+
248
+ def to_str
249
+ [order, time.to_i].join(',')
250
+ end
251
+ alias to_s to_str
252
+ end
253
+
254
+ Track = Base::Struct.new(
255
+ info: Chart::Info,
256
+ score: Either[Score, ScoreLite],
257
+ order: Integer,
258
+ time: Time,
259
+ challenge: Optional[Challenge],
260
+ )
261
+
262
+ TrackReference = Base::Struct.new(
263
+ track: Track,
264
+ ref_web_id: ReferenceWebID,
265
+ )
266
+
267
+ Data = Base::Struct.new(
268
+ track: Track,
269
+ breakdown: Generic[Hash, Symbol, Judgment],
270
+ timing: Offset,
271
+ members: Generic[Array, TourMember],
272
+ rival: Optional[RivalInfo],
273
+ )
274
+ end
275
+
276
+ module Record
277
+ History = Base::Struct.new(
278
+ play_count: Integer,
279
+ last_played: Time,
280
+ )
281
+
282
+ ScoreOnly = Base::Struct.new(
283
+ score: Float,
284
+ grade: Symbol,
285
+ )
286
+
287
+ Score = Base::Struct.new(
288
+ web_id: WebID,
289
+ score: Float,
290
+ deluxe_score: Result::Progress,
291
+ grade: Symbol,
292
+ deluxe_grade: Integer,
293
+ flags: Generic[Array, Symbol],
294
+ )
295
+
296
+ ChartRecord = Base::Struct.new(
297
+ info: Chart::Info,
298
+ record: Optional[Score],
299
+ history: Optional[History],
300
+ )
301
+
302
+ InfoCategory = Base::Struct.new(
303
+ info: Chart::Info,
304
+ score: Optional[Result::ScoreLite],
305
+ )
306
+
307
+ InfoBest = Base::Struct.new(
308
+ info: Chart::Info,
309
+ play_count: Integer,
310
+ )
311
+
312
+ InfoRating = Base::Struct.new(
313
+ info: Chart::Info,
314
+ score: ScoreOnly,
315
+ )
316
+
317
+ Data = Base::Struct.new(
318
+ info: Chart::Song,
319
+ charts: Generic[Hash, Symbol, ChartRecord],
320
+ )
321
+ end
322
+
323
+ module FinaleArchive
324
+ Decoration = Base::Struct.new(
325
+ icon: URI::Generic,
326
+ player_frame: URI::Generic,
327
+ nameplate: URI::Generic,
328
+ )
329
+ Currency = Base::Struct.new(
330
+ amount: Integer, piece: Integer, parts: Integer,
331
+ )
332
+ ExtendedInfo = Base::Struct.new(
333
+ rating: Float, rating_highest: Float,
334
+ region_count: Integer,
335
+ currency: Currency,
336
+ partner_level_total: Integer,
337
+ )
338
+
339
+ DifficultyStatistic = Base::Struct.new(
340
+ total_score: Integer,
341
+ clears: SongCount,
342
+ ranks: Generic[Hash, Symbol, SongCount],
343
+ flags: Generic[Hash, Symbol, SongCount],
344
+ sync_flags: Generic[Hash, Symbol, SongCount],
345
+ multi_flags: Generic[Hash, Symbol, SongCount],
346
+ )
347
+
348
+ Data = Base::Struct.new(
349
+ info: PlayerCommon::Info,
350
+ decoration: Decoration,
351
+ extended: ExtendedInfo,
352
+
353
+ statistics: Generic[Hash, Symbol, DifficultyStatistic],
354
+ )
355
+ end
356
+
357
+ private_constant :Base
358
+ end
359
+ end