renshuu 0.1.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: 182e63b036401b107a66bf329425dd16dbc35ae63dc0326996ccae79ee95484c
4
- data.tar.gz: 2ffe79bf4e6a7d9e22bf15cfe698b88b441a1ef7cfc185e4ee93d0438555ff59
3
+ metadata.gz: df6e1af568b7bb56df71d9bd55a92b9a64e0319e4a0daa6df5afec0a4c2d905e
4
+ data.tar.gz: 06fe422633574facfb49f0c9fbc53b52969026fe9df4fd8f50cf7904768ab379
5
5
  SHA512:
6
- metadata.gz: b480fa63633cdb81180c5703204b8867efa75903f5e9d197df99979064a2fffd5eb314ab01cece8c237dd4cd4826de58d2dd3cbd18700ff0683208b4d869250e
7
- data.tar.gz: 0dbf167eb256825633f0cb9069997042e80869e3e58eacb7b42f8cfd25db7cc015e8e356f834d4b586a59239981805481ab4f8d7ec06b85c9240e3a7f83c22c4
6
+ metadata.gz: 73dd997e5c0ef3fcd15de076192d660ee36158f7ac8bc54a54d3ce98281358595ae5b9523a8918fd66d2e5a9ebedd04d24c345137fe54e3b2f851ae5733f6813
7
+ data.tar.gz: 2c639cbe92edd74939c8b9ca2785c64a68b0d827c61f50d638e26605dff70169c1354893fc2a05ff976e4d6a40e5530145889520691555d18719caf714a1050d
data/.rubocop.yml CHANGED
@@ -9,6 +9,10 @@ AllCops:
9
9
  NewCops: enable
10
10
  TargetRubyVersion: 3.3
11
11
 
12
+ Layout/CaseIndentation:
13
+ EnforcedStyle: end
14
+ Layout/EndAlignment:
15
+ EnforcedStyleAlignWith: start_of_line
12
16
  Layout/LineLength:
13
17
  Max: 80
14
18
  Layout/MultilineMethodCallIndentation:
@@ -22,4 +26,4 @@ RSpec/NestedGroups:
22
26
  Max: 4
23
27
  RSpec/SpecFilePathFormat:
24
28
  Exclude:
25
- - spec/renshuu/models/*_spec.rb
29
+ - spec/renshuu/models/**/*_spec.rb
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.0] - 2025-12-09
4
+
5
+ ### Added
6
+
7
+ - Schedules
8
+ - Lists
9
+
10
+ ## [0.2.0] - 2025-12-03
11
+
12
+ ### Added
13
+
14
+ - Word dictionary and lookup
15
+ - Grammar dictionary and lookup
16
+ - Sentence dictionary and lookup
17
+
3
18
  ## [0.1.0] - 2025-07-31
4
19
 
5
20
  ### 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
 
@@ -61,6 +70,120 @@ Renshuu::Kanji.search('たべる')
61
70
  Renshuu::Kanji.get('紅')
62
71
  ```
63
72
 
73
+ ### Vocabulary dictionary
74
+
75
+ ```ruby
76
+ # Searches the word dictionary
77
+ Renshuu::Word.search('to eat')
78
+ Renshuu::Word.search('taberu')
79
+ Renshuu::Word.search('食べる')
80
+
81
+ # Retrieves a word from its identifier
82
+ Renshuu::Word.get(370)
83
+ ```
84
+
85
+ ### Grammar dictionary
86
+
87
+ ```ruby
88
+ # Searches the grammar dictionary
89
+ Renshuu::Grammar.search('without')
90
+ Renshuu::Grammar.search('ましょう')
91
+
92
+ # Retrieves a specific grammar entry from its identifier
93
+ Renshuu::Grammar.get(6)
94
+ ```
95
+
96
+ ### Sentence dictionary
97
+
98
+ ```ruby
99
+ # Searches the sentence dictionary
100
+ Renshuu::Sentence.search('to eat')
101
+ Renshuu::Sentence.search('ケーキ')
102
+
103
+ # Retrieves sentences for a given word.
104
+ Renshuu::Sentence.word_search(370)
105
+
106
+ # Syntactic sugar for `Renshuu::Word`.
107
+ word = Renshuu::Word.get(370)
108
+ word.sentences
109
+ ```
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
+
64
187
  ## Development
65
188
 
66
189
  After checking out the repo, run `bin/setup` to install dependencies. Then, run
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Renshuu
4
+ ##
5
+ # Model class for grammar entries.
6
+ class Grammar < Model
7
+ include Schedulable
8
+ include Listable
9
+
10
+ ##
11
+ # Searches the grammar dictionary.
12
+ #
13
+ # @param [String] value
14
+ #
15
+ # @return [Array<Grammar>]
16
+ def self.search(value)
17
+ Renshuu.client.query(:get, 'v1/grammar/search', params: { value: })
18
+ .fetch(:grammar).map { new(_1) }
19
+ end
20
+
21
+ ##
22
+ # Retrieves a grammar entry fom the dictionary.
23
+ #
24
+ # @param [Integer] id
25
+ #
26
+ # @return [Grammar]
27
+ def self.get(id)
28
+ Renshuu.client.query(:get, "v1/grammar/#{id}").then { new(_1) }
29
+ end
30
+
31
+ ##
32
+ # @!attribute [r] id
33
+ # @return [String]
34
+ attribute :id, Types::String
35
+ ##
36
+ # @!attribute [r] title_english
37
+ # @return [String]
38
+ attribute :title_english, Types::String
39
+ ##
40
+ # @!attribute [r] title_japanese
41
+ # @return [String]
42
+ attribute :title_japanese, Types::String
43
+ ##
44
+ # @!attribute [r] meaning
45
+ # @return [Hash{String => String}]
46
+ attribute :meaning, Types::Hash.map(Types::String, Types::String)
47
+ ##
48
+ # @!attribute [r] meaning_long
49
+ # @return [Hash{String => String}]
50
+ attribute :meaning_long, Types::Hash.map(Types::String, Types::String)
51
+
52
+ ##
53
+ # Model class for grammar models.
54
+ class Model < Model
55
+ ##
56
+ # @!attribute [r] japanese
57
+ # @return [String]
58
+ attribute :japanese, Types::String
59
+ ##
60
+ # @!attribute [r] hiragana
61
+ # @return [String]
62
+ attribute :hiragana, Types::String
63
+ ##
64
+ # @!attribute [r] meanings
65
+ # @return [Hash{String => String}]
66
+ attribute :meanings, Types::Hash.map(Types::String, Types::String)
67
+ end
68
+ ##
69
+ # @!attribute [r] models
70
+ # @return [Array<Model>]
71
+ attribute(:models, Types::Array.of(Model).default { [] })
72
+
73
+ ##
74
+ # @!attribute [r] user_data
75
+ # @return [UserData, nil]
76
+ attribute? :user_data, UserData
77
+ ##
78
+ # @!attribute [r] presence
79
+ # @return [Presence, nil]
80
+ attribute? :presence, Presence
81
+ end
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
  #
@@ -29,5 +32,82 @@ module Renshuu
29
32
  body = Renshuu.client.query(:get, "v1/kanji/#{cgi_character}")
30
33
  new(body)
31
34
  end
35
+
36
+ ##
37
+ # @!attribute [r] id
38
+ # @return [String]
39
+ attribute :id, Types::String
40
+ ##
41
+ # @!attribute [r] kanji
42
+ # @return [String]
43
+ attribute :kanji, Types::String
44
+ ##
45
+ # @!attribute [r] definition
46
+ # @return [String]
47
+ attribute :definition, Types::String
48
+ ##
49
+ # @!attribute [r] scount
50
+ # @return [Integer, nil]
51
+ attribute? :scount, Types::Integer
52
+ alias stroke_count scount
53
+ ##
54
+ # @!attribute [r] radical
55
+ # @return [String, nil]
56
+ attribute? :radical, Types::String
57
+ ##
58
+ # @!attribute [r] radical_name
59
+ # @return [String, nil]
60
+ attribute? :radical_name, Types::String
61
+ ##
62
+ # @!attribute [r] onyomi
63
+ # @return [String, nil]
64
+ attribute? :onyomi, Types::String
65
+ ##
66
+ # @!attribute [r] kunyomi
67
+ # @return [String, nil]
68
+ attribute? :kunyomi, Types::String
69
+ ##
70
+ # @!attribute [r] kanken
71
+ # @return [String, nil]
72
+ attribute? :kanken, Types::String
73
+ ##
74
+ # @!attribute [r] jlpt
75
+ # @return [String, nil]
76
+ attribute? :jlpt, Types::String
77
+
78
+ ##
79
+ # Model class for kanji's related words.
80
+ class RelatedWord < Model
81
+ ##
82
+ # @!attribute [r] reading
83
+ # @return [String]
84
+ attribute :reading, Types::String
85
+
86
+ ##
87
+ # Model class for kanji's related words' word.
88
+ class Word < Model
89
+ ##
90
+ # @!attribute [r] def
91
+ # @return [String]
92
+ attribute :def, Types::String
93
+ ##
94
+ # @!attribute [r] term
95
+ # @return [String]
96
+ attribute :term, Types::String
97
+ end
98
+ attribute :words, Types::Array.of(Word)
99
+ end
100
+ ##
101
+ # @!attribute [r] rwords
102
+ # @return [Array<RelatedWord>, nil]
103
+ attribute? :rwords, Types::Array.of(RelatedWord)
104
+ ##
105
+ # @!attribute [r] user_data
106
+ # @return [UserData, nil]
107
+ attribute? :user_data, UserData
108
+ ##
109
+ # @!attribute [r] presence
110
+ # @return [Presence, nil]
111
+ attribute? :presence, Presence
32
112
  end
33
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
@@ -3,13 +3,7 @@
3
3
  module Renshuu
4
4
  ##
5
5
  # Base class for model objects.
6
- class Model < RecursiveOpenStruct
7
- ##
8
- # Default options for new objects.
9
- #
10
- # @return [Hash{Symbol => Object}]
11
- def self.default_options
12
- super.merge(raise_on_missing: true, recurse_over_arrays: true)
13
- end
6
+ class Model < Dry::Struct
7
+ transform_keys { _1.to_s.underscore.to_sym }
14
8
  end
15
9
  end