renshuu 0.2.0 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a43b493faf4ee0d7165c0a6a054f04a1f2e50ecb60d81c1e9226bea9c6be3643
4
- data.tar.gz: 71e85c4b84336960eddd66b525167528495ba04fb26f340d6a022aa3bf5d5cff
3
+ metadata.gz: df6e1af568b7bb56df71d9bd55a92b9a64e0319e4a0daa6df5afec0a4c2d905e
4
+ data.tar.gz: 06fe422633574facfb49f0c9fbc53b52969026fe9df4fd8f50cf7904768ab379
5
5
  SHA512:
6
- metadata.gz: a848a7d5c6615157835467a7e564bfe201c13dfbf9f8ffc4ea26d6f15b3f292df68f042f3b0dd352391a8d6ba1577fdd2c68635946cd811aae15ba06b94ad89e
7
- data.tar.gz: e3eed0edab20ad32adba4042a6a4d4728c23fceb035ac2ba81704a64952e5aa1ff7ac6026ae2d7ac2bfbc59d6d5cf5aa7ebaa0b73893ecbd16dec7196d33b146
6
+ metadata.gz: 73dd997e5c0ef3fcd15de076192d660ee36158f7ac8bc54a54d3ce98281358595ae5b9523a8918fd66d2e5a9ebedd04d24c345137fe54e3b2f851ae5733f6813
7
+ data.tar.gz: 2c639cbe92edd74939c8b9ca2785c64a68b0d827c61f50d638e26605dff70169c1354893fc2a05ff976e4d6a40e5530145889520691555d18719caf714a1050d
data/.rubocop.yml CHANGED
@@ -26,4 +26,4 @@ RSpec/NestedGroups:
26
26
  Max: 4
27
27
  RSpec/SpecFilePathFormat:
28
28
  Exclude:
29
- - spec/renshuu/models/*_spec.rb
29
+ - spec/renshuu/models/**/*_spec.rb
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.0] - 2025-12-09
4
+
5
+ ### Added
6
+
7
+ - Schedules
8
+ - Lists
9
+
3
10
  ## [0.2.0] - 2025-12-03
4
11
 
5
12
  ### Added
data/README.md CHANGED
@@ -2,7 +2,16 @@
2
2
 
3
3
  Ruby wrapper for [Renshuu], the Japanese learning platform.
4
4
 
5
+ [![Pipeline status badge][pipeline]][pipelines]
6
+ [![Coverage badge][coverage]][pipelines]
7
+ [![Latest release badge][latest-release]][releases]
8
+
5
9
  [Renshuu]: https://renshuu.org
10
+ [pipeline]: https://gitlab.com/Richard-Degenne/renshuu/badges/dev/pipeline.svg?ignore_skipped=true
11
+ [pipelines]: https://gitlab.com/Richard-Degenne/renshuu/-/pipelines/
12
+ [coverage]: https://gitlab.com/Richard-Degenne/renshuu/badges/dev/coverage.svg
13
+ [latest-release]: https://gitlab.com/Richard-Degenne/renshuu/-/badges/release.svg
14
+ [releases]: https://gitlab.com/Richard-Degenne/renshuu/-/releases/
6
15
 
7
16
  ## Installation
8
17
 
@@ -99,6 +108,82 @@ word = Renshuu::Word.get(370)
99
108
  word.sentences
100
109
  ```
101
110
 
111
+ ### Study lists
112
+
113
+ ```ruby
114
+ # Lists the user's study lists
115
+ Renshuu::Schedule.list
116
+
117
+ # Retrieves a specific list
118
+ Renshuu::Schedule.get(36555)
119
+
120
+ # Retrieves the contents of a list
121
+ list = Renshuu::Schedule.get(36555)
122
+ list.contents
123
+ list.contents(page: 2, group: :review_today)
124
+
125
+ # Manage list content
126
+ kanji = Renshuu::Kanji.get('食')
127
+ kanji.add_to_list(list)
128
+ kanji.remove_from_list(list)
129
+ # Or
130
+ list.add(kanji)
131
+ list.remove(kanji)
132
+
133
+ word = Renshuu::Word.get(370)
134
+ word.add_to_list(list)
135
+ word.remove_from_list(list)
136
+ # Or
137
+ list.add(word)
138
+ list.remove(word)
139
+
140
+ grammar = Renshuu::Grammar.get(927)
141
+ grammar.add_to_list(list)
142
+ grammar.remove_from_list(list)
143
+ # Or
144
+ list.add(grammar)
145
+ list.remove(grammar)
146
+ ```
147
+
148
+ ### Lists
149
+
150
+ ```ruby
151
+ # Lists the user's lists
152
+ Renshuu::List.list
153
+
154
+ # Retrieves the contents of a list
155
+ list = Renshuu::List.list.first.lists.first # Browsing the list groups
156
+ list.contents
157
+ list.contents(page: 2)
158
+
159
+ # Manage list content
160
+ kanji = Renshuu::Kanji.get('食')
161
+ kanji.add_to_list(list)
162
+ kanji.remove_from_list(list)
163
+ # Or
164
+ list.add(kanji)
165
+ list.remove(kanji)
166
+
167
+ word = Renshuu::Word.get(370)
168
+ word.add_to_list(list)
169
+ word.remove_from_list(list)
170
+ # Or
171
+ list.add(word)
172
+ list.remove(word)
173
+
174
+ grammar = Renshuu::Grammar.get(927)
175
+ grammar.add_to_list(list)
176
+ grammar.remove_from_list(list)
177
+ # Or
178
+ list.add(grammar)
179
+ list.remove(grammar)
180
+ ```
181
+
182
+ ## Documentation
183
+
184
+ A comprehensive documenation is available at
185
+ <https://renshuu.richarddegenne.fr>.
186
+
102
187
  ## Development
103
188
 
104
189
  After checking out the repo, run `bin/setup` to install dependencies. Then, run
@@ -4,6 +4,9 @@ module Renshuu
4
4
  ##
5
5
  # Model class for grammar entries.
6
6
  class Grammar < Model
7
+ include Schedulable
8
+ include Listable
9
+
7
10
  ##
8
11
  # Searches the grammar dictionary.
9
12
  #
@@ -65,11 +68,15 @@ module Renshuu
65
68
  ##
66
69
  # @!attribute [r] models
67
70
  # @return [Array<Model>]
68
- attribute :models, Types::Array.of(Model)
71
+ attribute(:models, Types::Array.of(Model).default { [] })
69
72
 
70
73
  ##
71
74
  # @!attribute [r] user_data
72
- # @return [UserData]
73
- attribute :user_data, UserData
75
+ # @return [UserData, nil]
76
+ attribute? :user_data, UserData
77
+ ##
78
+ # @!attribute [r] presence
79
+ # @return [Presence, nil]
80
+ attribute? :presence, Presence
74
81
  end
75
82
  end
@@ -6,6 +6,9 @@ module Renshuu
6
6
  #
7
7
  # @see https://api.renshuu.org/docs/#/Kanji
8
8
  class Kanji < Model
9
+ include Schedulable
10
+ include Listable
11
+
9
12
  ##
10
13
  # Searches the Renshuu kanji dictionary.
11
14
  #
@@ -102,58 +105,9 @@ module Renshuu
102
105
  # @!attribute [r] user_data
103
106
  # @return [UserData, nil]
104
107
  attribute? :user_data, UserData
105
-
106
108
  ##
107
- # Model class for kanji presence.
108
- class Presence < Model
109
- ##
110
- # Model class for kanji schedule presence.
111
- class Schedule < Model
112
- ##
113
- # @!attribute [r] sched_id
114
- # @return [String]
115
- attribute :sched_id, Types::String
116
- alias schedule_id sched_id
117
- ##
118
- # @!attribute [r] name
119
- # @return [String]
120
- attribute :name, Types::String
121
- ##
122
- # @!attribute [r] has_word
123
- # @return [Boolean]
124
- attribute :has_word, Types::Params::Bool
125
- end
126
- ##
127
- # @!attribute [r] scheds
128
- # @return [Array<Schedule>]
129
- attribute :scheds, Types::Array.of(Schedule)
130
- alias schedules scheds
131
-
132
- ##
133
- # Model class for kanji list presence.
134
- class List < Model
135
- ##
136
- # @!attribute [r] list_id
137
- # @return [String]
138
- attribute :list_id, Types::String
139
- ##
140
- # @!attribute [r] name
141
- # @return [String]
142
- attribute :name, Types::String
143
- ##
144
- # @!attribute [r] set_name
145
- # @return [String, nil]
146
- attribute? :set_name, Types::String
147
- ##
148
- # @!attribute [r] has_word
149
- # @return [Boolean]
150
- attribute :has_word, Types::Params::Bool
151
- end
152
- ##
153
- # @!attribute [r] lists
154
- # @return [Array<List>]
155
- attribute :lists, Types::Array.of(List)
156
- end
109
+ # @!attribute [r] presence
110
+ # @return [Presence, nil]
157
111
  attribute? :presence, Presence
158
112
  end
159
113
  end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Renshuu
4
+ ##
5
+ # Model class for lists.
6
+ #
7
+ # @see https://api.renshuu.org/docs/#/User
8
+ class List < Model
9
+ ##
10
+ # Mapping between list termtypes and model classes.
11
+ #
12
+ # @see #term_class
13
+ TERMTYPES = {
14
+ 'kanji' => Kanji, 'vocab' => Word, 'grammar' => Grammar,
15
+ 'sent' => Sentence
16
+ }.freeze
17
+
18
+ ##
19
+ # Retrives list groups from the API.
20
+ #
21
+ # @return [Array<Group>]
22
+ def self.list
23
+ body = Renshuu.client.query(:get, 'v1/lists')
24
+
25
+ body.fetch(:termtype_groups).flat_map do |termtype_group|
26
+ termtype = termtype_group.fetch(:termtype)
27
+ termtype_group.fetch(:groups).map { Group.new(termtype:, **_1) }
28
+ end
29
+ end
30
+
31
+ ##
32
+ # Model class for list groups, _a.k.a._ sets.
33
+ class Group < Model
34
+ ##
35
+ # @!attribute [r] group_title
36
+ # @return [String]
37
+ attribute :group_title, Types::String
38
+ alias title group_title
39
+ ##
40
+ # @!attribute [r] termtype
41
+ # @return [String]
42
+ attribute :termtype, Types::String
43
+
44
+ ##
45
+ # @!attribute [r] lists
46
+ # @return [Array<List>]
47
+ attribute :lists, Types::Array.of(List)
48
+ end
49
+
50
+ ##
51
+ # @!attribute [r] list_id
52
+ # @return [String]
53
+ attribute :list_id, Types::String
54
+ alias id list_id
55
+ ##
56
+ # @!attribute [r] termtype
57
+ # @return [String]
58
+ attribute :termtype, Types::String
59
+ ##
60
+ # @!attribute [r] title
61
+ # @return [String]
62
+ attribute :title, Types::String
63
+ ##
64
+ # @!attribute [r] description
65
+ # @return [String]
66
+ attribute :description, Types::String
67
+ ##
68
+ # @!attribute [r] num_terms
69
+ # @return [Integer]
70
+ attribute :num_terms, Types::Integer
71
+ ##
72
+ # @!attribute [r] privacy
73
+ # @return [String]
74
+ attribute :privacy, Types::String
75
+
76
+ ##
77
+ # Retrieves terms contained in the list.
78
+ #
79
+ # @param [Integer, nil] page
80
+ #
81
+ # @return [Array<Kanji,Word,Grammar,Sentence>] Depending on the type of list
82
+ def contents(page: nil)
83
+ params = { pg: page }.compact
84
+ body = Renshuu.client.query(:get, "v1/list/#{id}", params:)
85
+
86
+ body.dig(:contents, :terms).map { term_class.new(_1) }
87
+ end
88
+
89
+ ##
90
+ # Adds the given item to the list.
91
+ #
92
+ # @param [Listable] item
93
+ #
94
+ # @return [Void]
95
+ # @raise [ArgumentError] If the type of the item does not match the
96
+ # list's type
97
+ #
98
+ # @see Listable#add_to_list
99
+ def add(item)
100
+ unless item.is_a?(term_class)
101
+ raise ArgumentError, "Expected a `#{term_class}`, got a `#{item.class}`"
102
+ end
103
+
104
+ item.add_to_list(self)
105
+ end
106
+
107
+ ##
108
+ # Removes the given item from the list.
109
+ #
110
+ # @param [Listable] item
111
+ #
112
+ # @return [Void]
113
+ # @raise [ArgumentError] If the type of the item does not match the
114
+ # list's type
115
+ #
116
+ # @see Listable#remove_from_list
117
+ def remove(item)
118
+ unless item.is_a?(term_class)
119
+ raise ArgumentError, "Expected a `#{term_class}`, got a `#{item.class}`"
120
+ end
121
+
122
+ item.remove_from_list(self)
123
+ end
124
+
125
+ ##
126
+ # Model class to use for this list's terms.
127
+ #
128
+ # @return [Class]
129
+ # @raise [KeyError]
130
+ #
131
+ # @see ::TERMTYPES
132
+ def term_class
133
+ @term_class ||= TERMTYPES.fetch(termtype) do |key|
134
+ raise KeyError, "Unkown list type `#{key}`"
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Renshuu
4
+ ##
5
+ # Mixin for items that can be added to study {List}s.
6
+ #
7
+ # = Interface
8
+ #
9
+ # This mixin rely on the following interface:
10
+ #
11
+ # * +#id+: Identifier of the listable item;
12
+ # * +.base_route+: Path part used in URL construction.
13
+ module Listable
14
+ def self.included(base) # :nodoc:
15
+ super
16
+
17
+ base.extend(ClassMethods)
18
+ end
19
+
20
+ ##
21
+ # Adds the item to the given list.
22
+ #
23
+ # @param [List, String] list
24
+ #
25
+ # @return [Void]
26
+ def add_to_list(list)
27
+ list_id = case list
28
+ when List then list.id
29
+ else list
30
+ end
31
+
32
+ Renshuu.client.query(
33
+ :put, "v1/#{self.class.base_route}/#{id}", params: { list_id: }
34
+ )
35
+ end
36
+
37
+ ##
38
+ # Removes the item from the given list.
39
+ #
40
+ # @param [List, String] list
41
+ #
42
+ # @return [Void]
43
+ def remove_from_list(list)
44
+ list_id = case list
45
+ when List then list.id
46
+ else list
47
+ end
48
+
49
+ Renshuu.client.query(
50
+ :delete, "v1/#{self.class.base_route}/#{id}", params: { list_id: }
51
+ )
52
+ end
53
+
54
+ ##
55
+ # Module for class methods provided with the mixin.
56
+ module ClassMethods
57
+ ##
58
+ # Path part used in URL construction.
59
+ #
60
+ # Defaults to the class name, demodulized and underscored. Override to
61
+ # change behavior.
62
+ #
63
+ # @return [String]
64
+ def base_route
65
+ name.demodulize.underscore
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Renshuu
4
+ ##
5
+ # Mixin for items that can be added to study {Schedule}s.
6
+ #
7
+ # = Interface
8
+ #
9
+ # This mixin rely on the following interface:
10
+ #
11
+ # * +#id+: Identifier of the schedulable item;
12
+ # * +.base_route+: Path part used in URL construction.
13
+ module Schedulable
14
+ def self.included(base)
15
+ super
16
+
17
+ base.extend(ClassMethods)
18
+ end
19
+
20
+ ##
21
+ # Adds the item to the given schedule.
22
+ #
23
+ # @param [Schedule, String] schedule
24
+ #
25
+ # @return [Void]
26
+ def add_to_schedule(schedule)
27
+ sched_id = case schedule
28
+ when Schedule then schedule.id
29
+ else schedule
30
+ end
31
+
32
+ Renshuu.client.query(
33
+ :put, "v1/#{self.class.base_route}/#{id}", params: { sched_id: }
34
+ )
35
+ end
36
+
37
+ ##
38
+ # Removes the item from the given schedule.
39
+ #
40
+ # @param [Schedule, String] schedule
41
+ #
42
+ # @return [Void]
43
+ def remove_from_schedule(schedule)
44
+ sched_id = case schedule
45
+ when Schedule then schedule.id
46
+ else schedule
47
+ end
48
+
49
+ Renshuu.client.query(
50
+ :delete, "v1/#{self.class.base_route}/#{id}", params: { sched_id: }
51
+ )
52
+ end
53
+
54
+ ##
55
+ # Module for class methods provided with the mixin.
56
+ module ClassMethods
57
+ ##
58
+ # Path part used in URL construction.
59
+ #
60
+ # Defaults to the class name, demodulized and underscored. Override to
61
+ # change behavior.
62
+ #
63
+ # @return [String]
64
+ def base_route
65
+ name.demodulize.underscore
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Renshuu
4
+ ##
5
+ # Model class for term presence.
6
+ class Presence < Model
7
+ ##
8
+ # Model class for schedule presence.
9
+ class Schedule < Model
10
+ ##
11
+ # @!attribute [r] sched_id
12
+ # @return [String]
13
+ attribute :sched_id, Types::String
14
+ alias schedule_id sched_id
15
+ ##
16
+ # @!attribute [r] name
17
+ # @return [String]
18
+ attribute :name, Types::String
19
+ ##
20
+ # @!attribute [r] has_word
21
+ # @return [Boolean]
22
+ attribute :has_word, Types::Params::Bool
23
+ alias has_word? has_word
24
+
25
+ ##
26
+ # Retrieves the schedule associated with this presence item.
27
+ #
28
+ # @return [Renshuu::Schedule]
29
+ #
30
+ # @see Renshuu::Schedule.get
31
+ def schedule
32
+ Renshuu::Schedule.get(schedule_id)
33
+ end
34
+ end
35
+ ##
36
+ # @!attribute [r] scheds
37
+ # @return [Array<Schedule>]
38
+ attribute(:scheds, Types::Array.of(Schedule).default { [] })
39
+ alias schedules scheds
40
+
41
+ ##
42
+ # Model class for list presence.
43
+ class List < Model
44
+ ##
45
+ # @!attribute [r] list_id
46
+ # @return [String]
47
+ attribute :list_id, Types::String
48
+ ##
49
+ # @!attribute [r] name
50
+ # @return [String]
51
+ attribute :name, Types::String
52
+ ##
53
+ # @!attribute [r] set_name
54
+ # @return [String, nil]
55
+ attribute? :set_name, Types::String
56
+ ##
57
+ # @!attribute [r] has_word
58
+ # @return [Boolean]
59
+ attribute :has_word, Types::Params::Bool
60
+ end
61
+ ##
62
+ # @!attribute [r] lists
63
+ # @return [Array<List>]
64
+ attribute(:lists, Types::Array.of(List).default { [] })
65
+ end
66
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Renshuu
4
+ ##
5
+ # Model class for schedules.
6
+ #
7
+ # @see https://api.renshuu.org/docs/#/Schedules
8
+ class Schedule < Model
9
+ ##
10
+ # Mapping between schedule booktypes and model classes.
11
+ #
12
+ # @see #term_class
13
+ BOOKTYPES = {
14
+ 'kanji' => Kanji, 'vocab' => Word, 'grammar' => Grammar,
15
+ 'sent' => Sentence
16
+ }.freeze
17
+
18
+ ##
19
+ # Lists all schedules of the current user.
20
+ #
21
+ # @return [Array<Schedule>]
22
+ #
23
+ # @see https://api.renshuu.org/docs/#/Schedules/get_schedule
24
+ def self.list
25
+ body = Renshuu.client.query(:get, 'v1/schedule')
26
+
27
+ body.fetch(:schedules).map { new(_1) }
28
+ end
29
+
30
+ ##
31
+ # Retrieves a specific schedule from the API.
32
+ #
33
+ # @param [String, Integer] id
34
+ # @return [Schedule]
35
+ #
36
+ # @see https://api.renshuu.org/docs/#/Schedules/get_schedule__id_
37
+ def self.get(id)
38
+ body = Renshuu.client.query(:get, "v1/schedule/#{id}")
39
+
40
+ body.fetch(:schedules).first.then { new(_1) }
41
+ end
42
+
43
+ ##
44
+ # @!attribute [r] id
45
+ # @return [String]
46
+ attribute :id, Types::String
47
+ ##
48
+ # @!attribute [r] name
49
+ # @return [String]
50
+ attribute :name, Types::String
51
+ ##
52
+ # @!attribute [r] booktype
53
+ # @return [String]
54
+ attribute :booktype, Types::String
55
+ ##
56
+ # @!attribute [r] is_frozen
57
+ # @return [Boolean]
58
+ attribute :is_frozen, Types::Params::Bool
59
+
60
+ ##
61
+ # Model class for schedules today counts.
62
+ class TodayCount < Model
63
+ transform_keys do |key|
64
+ key == :new ? :new_terms : key
65
+ end
66
+
67
+ ##
68
+ # @!attribute [r] review
69
+ # @return [Integer]
70
+ attribute :review, Types::Integer
71
+ ##
72
+ # @!attribute [r] new_terms
73
+ # @note +new+ is a reserved keyword, so we have to circumvent that.
74
+ # @return [Integer]
75
+ attribute :new_terms, Types::Integer
76
+ end
77
+ ##
78
+ # @!attribute [r] today
79
+ # rn [TodayCount]
80
+ attribute :today, TodayCount
81
+
82
+ ##
83
+ # Model class for schedule upcoming counts.
84
+ class UpcomingCount < Model
85
+ ##
86
+ # @!attribute [r] days_in_future
87
+ # @return [Integer]
88
+ attribute :days_in_future, Types::Integer
89
+ ##
90
+ # @!attribute [r] terms_to_review
91
+ # @return [Integer]
92
+ attribute :terms_to_review, Types::Integer
93
+ end
94
+ ##
95
+ # @!attribute [r] upcoming
96
+ # @return [Array<UpcomingCount>]
97
+ attribute :upcoming, Types::Array.of(UpcomingCount)
98
+
99
+ ##
100
+ # Model class for schedule term counts.
101
+ class TermCount < Model
102
+ ##
103
+ # @!attribute [r] total_count
104
+ # @return [Integer]
105
+ attribute :total_count, Types::Integer
106
+ ##
107
+ # @!attribute [r] studied_count
108
+ # @return [Integer]
109
+ attribute :studied_count, Types::Integer
110
+ ##
111
+ # @!attribute [r] unstudied_count
112
+ # @return [Integer]
113
+ attribute :unstudied_count, Types::Integer
114
+ ##
115
+ # @!attribute [r] hidden_count
116
+ # @return [Integer]
117
+ attribute :hidden_count, Types::Integer
118
+ end
119
+ ##
120
+ # @!attribute [r] terms
121
+ # @return [TermCount]
122
+ attribute :terms, TermCount
123
+
124
+ ##
125
+ # Model class for schedule new term counts.
126
+ class NewTermCount < Model
127
+ ##
128
+ # @!attribute [r] today_count
129
+ # @return [Integer]
130
+ attribute :today_count, Types::Integer
131
+ ##
132
+ # @!attribute [r] rolling_week_count
133
+ # @return [Integer]
134
+ attribute :rolling_week_count, Types::Integer
135
+ end
136
+ ##
137
+ # @!attribute [r] new_terms
138
+ # @return [NewTermCount]
139
+ attribute :new_terms, NewTermCount
140
+
141
+ ##
142
+ # Retrieves the terms contained in the schedule.
143
+ #
144
+ # @param [Integer, nil] page
145
+ # @param [Symbol, nil] group One of +:all+, +:blocked+, +:studied+,
146
+ # +:notyetstudied+, +:cannot_study+, +:review_today+, +:mastery_1+,
147
+ # +:mastery_2+, +:mastery_3+, +:mastery_4+, +:mastery_5+, +:mastery_6+,
148
+ # +:mastery_7+, +:mastery_8+ or +:mastery_9+.
149
+ #
150
+ # @return [Array<Kanji,Word,Grammar,Sentence>] Depending on the type of
151
+ # schedule
152
+ def contents(page: nil, group: nil)
153
+ params = { pg: page, group: }.compact
154
+ body = Renshuu.client.query(:get, "v1/schedule/#{id}/list", params:)
155
+
156
+ body.dig(:contents, :terms).map { term_class.new(_1) }
157
+ end
158
+
159
+ ##
160
+ # Adds the given item to the schedule.
161
+ #
162
+ # @param [Schedulable] item
163
+ #
164
+ # @return [Void]
165
+ # @raise [ArgumentError] If the type of the item does not match the
166
+ # schedule's type
167
+ #
168
+ # @see Schedulable#add_to_schedule
169
+ def add(item)
170
+ unless item.is_a?(term_class)
171
+ raise ArgumentError, "Expected a `#{term_class}`, got a `#{item.class}`"
172
+ end
173
+
174
+ item.add_to_schedule(self)
175
+ end
176
+
177
+ ##
178
+ # Removes the given item from the schedule.
179
+ #
180
+ # @param [Schedulable] item
181
+ #
182
+ # @return [Void]
183
+ # @raise [ArgumentError] If the type of the item does not match the
184
+ # schedule's type
185
+ #
186
+ # @see Schedulable#remove_from_schedule
187
+ def remove(item)
188
+ unless item.is_a?(term_class)
189
+ raise ArgumentError, "Expected a `#{term_class}`, got a `#{item.class}`"
190
+ end
191
+
192
+ item.remove_from_schedule(self)
193
+ end
194
+
195
+ ##
196
+ # Model class to use for this schedule's terms.
197
+ #
198
+ # @return [Class]
199
+ # @raise [KeyError]
200
+ #
201
+ # @see ::BOOKTYPES
202
+ def term_class
203
+ @term_class ||= BOOKTYPES.fetch(booktype) do |key|
204
+ raise KeyError, "Unkown schedule type `#{key}`"
205
+ end
206
+ end
207
+ end
208
+ end
@@ -46,8 +46,8 @@ module Renshuu
46
46
  attribute :japanese, Types::String
47
47
  ##
48
48
  # @!attribute [r] hiragana
49
- # @return [String]
50
- attribute :hiragana, Types::String
49
+ # @return [String, nil]
50
+ attribute? :hiragana, Types::String
51
51
  ##
52
52
  # @!attribute [r] meaning
53
53
  # @return [Hash{String => String}]
@@ -9,12 +9,11 @@ module Renshuu
9
9
  ##
10
10
  # Dry type for dates.
11
11
  Date = Constructor(::Date) do |date|
12
- if date.is_a?(::Date)
13
- date
14
- elsif date == 'Not yet'
15
- nil
16
- else
17
- ::Date.parse(date)
12
+ case date
13
+ when ::Date then date
14
+ when 'Not yet' then nil
15
+ when 'Now' then ::Date.today
16
+ else ::Date.parse(date)
18
17
  end
19
18
  end
20
19
 
@@ -15,7 +15,7 @@ module Renshuu
15
15
  ##
16
16
  # @!attribute [r] mastery_avg_perc
17
17
  # @return [Integer]
18
- attribute :mastery_avg_perc, Types::Integer
18
+ attribute :mastery_avg_perc, Types::Integer.fallback(0)
19
19
 
20
20
  ##
21
21
  # Model class for user data study vectors.
@@ -6,6 +6,9 @@ module Renshuu
6
6
  #
7
7
  # @see https://api.renshuu.org/docs/#/Vocabulary
8
8
  class Word < Model
9
+ include Schedulable
10
+ include Listable
11
+
9
12
  ##
10
13
  # Searches the Renshuu vocabulary dictionary.
11
14
  #
@@ -44,13 +47,13 @@ module Renshuu
44
47
  attribute :hiragana_full, Types::String
45
48
  ##
46
49
  # @!attribute [r] pic
47
- # @return [Array<URI>, nil]
48
- attribute? :pic, Types::Array.of(Types::URI)
50
+ # @return [Array<URI>]
51
+ attribute(:pic, Types::Array.of(Types::URI).default { [] })
49
52
  alias pictures pic
50
53
  ##
51
54
  # @!attribute [r] markers
52
55
  # @return [Array<String>]
53
- attribute :markers, Types::Array.of(Types::String)
56
+ attribute(:markers, Types::Array.of(Types::String).default { [] })
54
57
  ##
55
58
  # @!attribute [r] pitch
56
59
  # @return [Array<String>]
@@ -69,6 +72,10 @@ module Renshuu
69
72
  # @!attribute [r] user_data
70
73
  # @return [UserData, nil]
71
74
  attribute? :user_data, UserData
75
+ ##
76
+ # @!attribute [r] presence
77
+ # @return [Presence, nil]
78
+ attribute? :presence, Presence
72
79
 
73
80
  ##
74
81
  # Retrieves sentences using this word from the dictionary.
@@ -6,10 +6,15 @@ end
6
6
  # Types
7
7
  require_relative 'models/types'
8
8
 
9
+ # Mixins
10
+ require_relative 'models/mixins/listable'
11
+ require_relative 'models/mixins/schedulable'
12
+
9
13
  # Base class
10
14
  require_relative 'models/model'
11
15
 
12
16
  # Other models (respect blank lines for dependency order)
17
+ require_relative 'models/presence'
13
18
  require_relative 'models/user_data'
14
19
 
15
20
  require_relative 'models/grammar'
@@ -17,3 +22,6 @@ require_relative 'models/kanji'
17
22
  require_relative 'models/profile'
18
23
  require_relative 'models/sentence'
19
24
  require_relative 'models/word'
25
+
26
+ require_relative 'models/list'
27
+ require_relative 'models/schedule'
@@ -3,5 +3,5 @@
3
3
  module Renshuu
4
4
  ##
5
5
  # Semantic version number of the library.
6
- VERSION = '0.2.0'
6
+ VERSION = '1.0.0'
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: renshuu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Degenne
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-12-03 00:00:00.000000000 Z
11
+ date: 2025-12-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-struct
@@ -62,8 +62,13 @@ files:
62
62
  - lib/renshuu/models.rb
63
63
  - lib/renshuu/models/grammar.rb
64
64
  - lib/renshuu/models/kanji.rb
65
+ - lib/renshuu/models/list.rb
66
+ - lib/renshuu/models/mixins/listable.rb
67
+ - lib/renshuu/models/mixins/schedulable.rb
65
68
  - lib/renshuu/models/model.rb
69
+ - lib/renshuu/models/presence.rb
66
70
  - lib/renshuu/models/profile.rb
71
+ - lib/renshuu/models/schedule.rb
67
72
  - lib/renshuu/models/sentence.rb
68
73
  - lib/renshuu/models/types.rb
69
74
  - lib/renshuu/models/user_data.rb