jp_address_complement 0.1.0 → 0.3.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: 17438b03576a1b31a50ee4ed00413a2da5ccd8e2187c3ccb1b4f60f5f9a8c0df
4
- data.tar.gz: 1efaf00afc7166bdb9b6bfcdde8128ea9e7d9682d5500c1096d9d8eb7e166ed9
3
+ metadata.gz: 5ac0fd79d7ab6a6cfead9398aa3907ccfcbfe6766e59a3653b768035f057e5ce
4
+ data.tar.gz: be6413cd0b12304bf00259f3a8bbacf68051bdaa6f7d74eaf54718550c4f0a9e
5
5
  SHA512:
6
- metadata.gz: dc70f4d677374348176ae4a724aee2fba171fc101cde7c2c091a2c8d32d594a6ac51d7575c16fedf5ae9e7babda75f1fd575e7072f2062beb7105c45798e5ef0
7
- data.tar.gz: dab79d95277591fec2a31775b9b436397900ed7cd889f9db5016008286efcdfb68021d91771573f26e858fbde6abf36b691b6ed7e566cbe14ab14d5a65f7b449
6
+ metadata.gz: 7ab89ad98dc9510ba4a9524dc4683211ecb39562b8a91b669164c4e2c7da82d515ebaa9a9a780fd5c385ca8c228a1ac0b0673e21678deeea9b9e3f6271ce836a
7
+ data.tar.gz: bc901f201de85ffd2aedca0613c07dd988f478927ff91f2220e9195f5587fba695a78ea8c16ca25971e64c3186dca69ba5ca15f70776fb0de3ba4333ca9a84f9
data/CHANGELOG.md CHANGED
@@ -1,25 +1,36 @@
1
1
  ## [Unreleased]
2
2
 
3
- ### Changed
3
+ ## [0.3.0] - 2026-03-02
4
+
5
+ ### Security
4
6
 
5
- - **CSV インポート(003: 消えたレコード削除)**
6
- - `JpAddressComplement::Importers::CsvImporter#import` の戻り値を **Integer から `ImportResult`(Data.define :upserted, :deleted)に変更**(破壊的変更)
7
- - フルインポート時に、今回のCSVに存在しない住所レコードをストアから削除するようにした
8
- - 空CSV(有効行0件)の場合はインポートを拒否し、`JpAddressComplement::ImportError` を発生させる
9
- - Rake タスク `jp_address_complement:import` の完了メッセージを「インポート完了: upsert N 件, 削除 M 件」形式に変更
7
+ - **Zip Slip 脆弱性の修正**: 解凍先パスの検証を強化し、Zip Slip 攻撃を防止。あわせて `Dir.chdir` によるスレッド安全性の問題を解消
10
8
 
11
9
  ### Added
12
10
 
13
- - **都道府県コード変換・住所逆引き(004)**
14
- - `JpAddressComplement.prefecture_name_from_code(code)` — 都道府県コード(2桁)から正式名称を返す(該当なしは nil)
15
- - `JpAddressComplement.prefecture_code_from_name(name)` — 都道府県の正式名称から2桁コードを返す(該当なしは nil)
16
- - `JpAddressComplement.search_postal_codes_by_address(pref:, city:, town: nil)` — 都道府県・市区町村・町域から郵便番号候補を逆引き(該当なし・入力不十分時は [])
11
+ - 町域なしの住所も `valid_combination?` で一致とみなすように変更(テスト追加)
12
+
13
+ ### Changed
14
+
15
+ - **ActiveSupport 依存の廃止**: `blank?` をネイティブ Ruby に置換
16
+ - **upsert_batch の改善**: N+1 DELETE を Arel バッチ削除に変更し、SQLite の式木深度上限エラーに対応(`DELETE_CHUNK_SIZE` 導入)
17
+ - **COL_* 定数の整理**: KenAll モジュールに集約し重複を解消
18
+ - README: `search_postal_codes_by_address` の戻り値を正しく記載
19
+
20
+ ### Fixed
21
+
22
+ - RuboCop 違反の修正(Metrics/AbcSize 含む)および RBS シグネチャの更新
23
+
24
+ ## [0.2.0] - 2026-03-02
25
+
26
+ ### Changed
17
27
 
18
- - RBS type annotations and Steep type checking (see [specs/002-rbs-type-annotations](../specs/002-rbs-type-annotations/))
19
- - Public API and internal methods annotated with rbs-inline
20
- - `sig/` and `sig/manual/` for type definitions; `AddressRecord` hand-defined in `sig/manual/address_record.rbs`
21
- - Rake tasks: `rake rbs:generate` (generate RBS from annotations), `rake steep` (type check)
22
- - CI runs `bundle exec rake steep`; type errors fail the build
28
+ - **Active Record をオプショナルに変更**
29
+ - gemspec: `activerecord` / `railties` runtime 依存から削除し、development 依存に移動
30
+ - デフォルトリポジトリ・`base_record_class`: Gemfile のみで未 require の場合に備え、require を試してから利用するよう変更(`defined?` 判定を廃止)
31
+ - Railtie: 同様に `require 'rails'` を試してから読み込むよう変更
32
+ - `default_repository`: LoadError 時に利用者向けメッセージで `JpAddressComplement::Error` を発生
33
+ - README: 必要環境・インストール手順に利用者側の対応を追記
23
34
 
24
35
  ## [0.1.0] - 2026-02-22
25
36
 
data/README.md CHANGED
@@ -26,8 +26,7 @@
26
26
  ## 必要環境
27
27
 
28
28
  - Ruby 3.2 以上
29
- - Rails 7.1 以上
30
- - ActiveRecord 利用を前提としています
29
+ - **デフォルトの DB 利用(ActiveRecord)を使う場合のみ**: Rails 7.0 以上および `activerecord` gem(Rails アプリでは通常含まれる)。使わない場合は不要です。
31
30
 
32
31
  ## インストール
33
32
 
@@ -39,13 +38,24 @@
39
38
  gem 'jp_address_complement'
40
39
  ```
41
40
 
41
+ **デフォルトの ActiveRecord リポジトリを使う場合(DB に郵便番号テーブルを置く場合)**
42
+ 本 gem は `activerecord` を必須依存にしていません。Rails アプリでは `gem 'rails'` に含まれるため追加不要です。Rails を使わずに ActiveRecord だけ使う場合は、利用者側で `Gemfile` に `activerecord` を追加してください。
43
+
44
+ ```ruby
45
+ gem 'jp_address_complement'
46
+ gem 'activerecord', '>= 7.0' # デフォルトリポジトリを使う場合のみ
47
+ ```
48
+
49
+ **CSV リポジトリや自前リポジトリだけ使う場合**
50
+ `JpAddressComplement.configure { |c| c.repository = ... }` でリポジトリを設定すれば、`activerecord` は不要です。gem の追加だけで利用できます。
51
+
42
52
  Bundler を使わない場合:
43
53
 
44
54
  ```bash
45
55
  gem install jp_address_complement
46
56
  ```
47
57
 
48
- ### 2. テーブルの作成
58
+ ### 2. テーブルの作成(デフォルトの ActiveRecord リポジトリを使う場合)
49
59
 
50
60
  郵便番号データを格納するマイグレーションを生成し、マイグレートします。
51
61
 
@@ -54,7 +64,7 @@ rails g jp_address_complement:install
54
64
  rails db:migrate
55
65
  ```
56
66
 
57
- ### 3. 住所データのインポート
67
+ ### 3. 住所データのインポート(デフォルトの ActiveRecord リポジトリを使う場合)
58
68
 
59
69
  日本郵便の KEN_ALL 形式 CSV(UTF-8 版 `utf_ken_all.csv`)をインポートする必要があります。
60
70
 
@@ -125,7 +135,7 @@ records = JpAddressComplement.search_by_postal_code_prefix('1000')
125
135
 
126
136
  ### 郵便番号と住所の整合性チェック
127
137
 
128
- 「この郵便番号とこの住所の組み合わせは正しいか?」を判定します。フォームのバリデーションに利用できます。
138
+ 「この郵便番号とこの住所の組み合わせは正しいか?」を判定します。フォームのバリデーションに利用できます。町域まで含む場合は町域まで、市区町村までしか含まれていない場合は市区町村までのチェックとなります。
129
139
 
130
140
  ```ruby
131
141
  JpAddressComplement.valid_combination?('1000001', '東京都千代田区千代田')
@@ -177,12 +187,16 @@ JpAddressComplement.prefecture_code_from_name('東京都')
177
187
  都道府県・市区町村・町域を指定して、対応する郵便番号の一覧を取得します。
178
188
 
179
189
  ```ruby
180
- codes = JpAddressComplement.search_postal_codes_by_address(
190
+ results = JpAddressComplement.search_postal_codes_by_address(
181
191
  pref: '東京都',
182
192
  city: '千代田区',
183
193
  town: '千代田'
184
194
  )
185
- # => ["1000001"] など、該当する郵便番号の配列
195
+ # => [["1000001", #<JpAddressComplement::AddressRecord postal_code="1000001", pref="東京都", city="千代田区", town="千代田", ...>]]
196
+
197
+ # 郵便番号のみが必要な場合
198
+ results.map(&:first)
199
+ # => ["1000001"]
186
200
  ```
187
201
 
188
202
  `town` は省略可能です。その場合は都道府県+市区町村で検索されます。
@@ -1,10 +1,8 @@
1
1
  PATH
2
2
  remote: ../../..
3
3
  specs:
4
- jp_address_complement (0.1.0)
5
- activerecord (>= 7.0)
4
+ jp_address_complement (0.3.0)
6
5
  csv (>= 3.0)
7
- railties (>= 7.0)
8
6
  rubyzip (>= 2.3)
9
7
 
10
8
  GEM
@@ -4,6 +4,7 @@
4
4
  require 'csv'
5
5
  require_relative '../address_record'
6
6
  require_relative '../models/postal_code'
7
+ require_relative '../ken_all'
7
8
 
8
9
  module JpAddressComplement
9
10
  module Importers
@@ -15,20 +16,16 @@ module JpAddressComplement
15
16
  # UTF-8 版 KEN_ALL(utf_ken_all.csv)を読み込み、jp_address_complement_postal_codes テーブルに upsert する
16
17
  # UTF-8 形式の CSV を前提として処理する
17
18
  class CsvImporter
18
- BATCH_SIZE = 1000 #: Integer
19
+ include KenAll
20
+
21
+ # バッチ削除・ユニークキーとして使うカラム群
22
+ KEY_COLUMNS = %i[postal_code pref_code city town kana_pref kana_city kana_town].freeze #: Array[Symbol]
19
23
 
20
- # 列インデックス(KEN_ALL.CSV 形式)
21
- COL_PREF_CODE = 0 #: Integer
22
- COL_POSTAL_CODE = 2 #: Integer
23
- COL_KANA_PREF = 3 #: Integer
24
- COL_KANA_CITY = 4 #: Integer
25
- COL_KANA_TOWN = 5 #: Integer
26
- COL_PREF = 6 #: Integer
27
- COL_CITY = 7 #: Integer
28
- COL_TOWN = 8 #: Integer
29
- COL_IS_PARTIAL = 9 #: Integer
30
- COL_HAS_ALIAS = 12 #: Integer
31
- COL_IS_LARGE_OFFICE = 13 #: Integer
24
+ # SQLite のデフォルト式木深度制限(1000)対策。
25
+ # reduce(:or) N 件 OR 結合すると深さ ≈ N + 6 になるため、500 件に収める(深さ ≈ 506)。
26
+ DELETE_CHUNK_SIZE = 500 #: Integer
27
+
28
+ BATCH_SIZE = 1000 #: Integer
32
29
 
33
30
  # @rbs (String csv_path) -> void
34
31
  def initialize(csv_path)
@@ -108,22 +105,24 @@ module JpAddressComplement
108
105
  # @rbs (Array[Hash[Symbol, untyped]] batch) -> void
109
106
  def upsert_batch(batch)
110
107
  PostalCode.transaction do
111
- batch.each do |record|
112
- PostalCode.where(
113
- postal_code: record[:postal_code],
114
- pref_code: record[:pref_code],
115
- city: record[:city],
116
- town: record[:town],
117
- kana_pref: record[:kana_pref],
118
- kana_city: record[:kana_city],
119
- kana_town: record[:kana_town]
120
- ).delete_all
121
- end
122
-
108
+ batch_delete(batch)
123
109
  PostalCode.upsert_all(batch)
124
110
  end
125
111
  end
126
112
 
113
+ # バッチ内の全レコードを1クエリで一括削除する
114
+ # Arel を使うことで NULL カラム(town 等)を IS NULL として正しく扱う
115
+ # @rbs (Array[Hash[Symbol, untyped]] batch) -> void
116
+ def batch_delete(batch)
117
+ table = PostalCode.arel_table
118
+ batch.each_slice(DELETE_CHUNK_SIZE) do |chunk|
119
+ conditions = chunk.map do |record|
120
+ KEY_COLUMNS.map { |col| table[col].eq(record[col]) }.reduce(:and)
121
+ end.reduce(:or)
122
+ PostalCode.where(conditions).delete_all
123
+ end
124
+ end
125
+
127
126
  # 郵便番号・都道府県・市区町村・町域(漢字)が同じでも読み(カナ)が異なれば別レコードとして扱う
128
127
  # @rbs (Hash[Symbol, untyped] record) -> Array[String]
129
128
  def row_key(record)
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module JpAddressComplement
5
+ # UTF-8 版 KEN_ALL(utf_ken_all.csv)のフォーマット定数
6
+ #
7
+ # CSV の各列インデックスを定義する。include することでクラス内から直接参照できる。
8
+ #
9
+ # 参考: https://www.post.japanpost.jp/zipcode/dl/readme.html
10
+ module KenAll
11
+ # 列インデックス(KEN_ALL.CSV 形式)
12
+ COL_PREF_CODE = 0 # : Integer
13
+ COL_POSTAL_CODE = 2 # : Integer
14
+ COL_KANA_PREF = 3 # : Integer
15
+ COL_KANA_CITY = 4 # : Integer
16
+ COL_KANA_TOWN = 5 # : Integer
17
+ COL_PREF = 6 # : Integer
18
+ COL_CITY = 7 # : Integer
19
+ COL_TOWN = 8 # : Integer
20
+ COL_IS_PARTIAL = 9 # : Integer
21
+ COL_HAS_ALIAS = 12 # : Integer
22
+ COL_IS_LARGE_OFFICE = 13 # : Integer
23
+ end
24
+ end
@@ -94,21 +94,35 @@ module JpAddressComplement
94
94
  def extract_zip_entries(zip_path, tmpdir)
95
95
  csv_path = nil
96
96
  Zip::File.open(zip_path) do |zip_file|
97
- Dir.chdir(tmpdir) do
98
- zip_file.each do |entry|
99
- name = entry.name.delete_prefix('/')
100
- dir = File.dirname(name)
101
- FileUtils.mkdir_p(dir) unless dir == '.'
102
- # RubyZip::Entry#extract(dest_path = nil) を使い、既存ファイルがあればブロックで上書き許可する。
103
- entry.extract(name) { true } # steep:ignore
104
- dest = File.join(tmpdir, name)
105
- csv_path = dest if entry_csv?(name, dest)
106
- end
97
+ zip_file.each do |entry|
98
+ dest = safe_extract_dest(tmpdir, entry.name)
99
+ next if dest.nil? || dest == tmpdir # パストラバーサルの可能性があるエントリはスキップ
100
+
101
+ FileUtils.mkdir_p(File.dirname(dest))
102
+ # RubyZip 3.x: 第1引数は destination_directory 配下への相対パス、
103
+ # destination_directory: で展開先ディレクトリを指定する。
104
+ # safe_extract_dest で検証済みの相対パスを渡し、パストラバーサルを多層防御する。
105
+ relative_path = dest[(tmpdir.length + 1)..]
106
+ entry.extract(relative_path, destination_directory: tmpdir) { true } # steep:ignore
107
+ csv_path = dest if entry_csv?(entry.name, dest)
107
108
  end
108
109
  end
109
110
  csv_path
110
111
  end
111
112
 
113
+ # ZIP エントリの展開先絶対パスを安全に解決する。
114
+ # entry_name に含まれる ../ などにより tmpdir の外を指す場合は nil を返す(Zip Slip 対策)。
115
+ # @rbs (String tmpdir, String entry_name) -> String?
116
+ def safe_extract_dest(tmpdir, entry_name)
117
+ # 先頭の / を除去して相対パスとして扱い、File.expand_path で ../ を解決する
118
+ relative = entry_name.delete_prefix('/')
119
+ dest = File.expand_path(File.join(tmpdir, relative))
120
+ # tmpdir 配下に収まるエントリのみ許可する
121
+ return nil unless dest.start_with?("#{tmpdir}#{File::SEPARATOR}") || dest == tmpdir
122
+
123
+ dest
124
+ end
125
+
112
126
  # @rbs (String entry_name, String dest) -> bool
113
127
  def entry_csv?(entry_name, dest)
114
128
  File.basename(entry_name) == CSV_FILENAME && File.file?(dest)
@@ -21,7 +21,7 @@ module JpAddressComplement
21
21
  # @param code [String, nil] 郵便番号文字列(ハイフン・全角・〒記号を自動除去)
22
22
  # @return [String, nil] 正規化後の7桁郵便番号。不正な場合は nil
23
23
  def normalize_postal_code(code)
24
- return nil if code.blank?
24
+ return nil if code.nil? || code.strip.empty?
25
25
 
26
26
  normalized = normalize_string(code)
27
27
  return nil unless normalized.match?(DIGIT_ONLY)
@@ -34,7 +34,7 @@ module JpAddressComplement
34
34
  # @param prefix [String, nil] 郵便番号の先頭部分(4桁以上)
35
35
  # @return [String, nil] 正規化後の数字文字列。4桁未満または不正な場合は nil
36
36
  def normalize_prefix(prefix)
37
- return nil if prefix.blank?
37
+ return nil if prefix.nil? || prefix.strip.empty?
38
38
 
39
39
  normalized = normalize_string(prefix)
40
40
  return nil if normalized.empty?
@@ -14,6 +14,8 @@ module JpAddressComplement
14
14
  end
15
15
 
16
16
  initializer 'jp_address_complement.setup_repository' do # steep:ignore
17
+ next unless defined?(ActiveRecord)
18
+
17
19
  require_relative 'repositories/active_record_postal_code_repository'
18
20
  require_relative 'models/postal_code'
19
21
  JpAddressComplement.configuration.repository ||=
@@ -35,11 +35,11 @@ module JpAddressComplement
35
35
 
36
36
  # @rbs (pref: String?, city: String?, ?town: String?) -> untyped
37
37
  def address_relation(pref:, city:, town: nil)
38
- return nil if pref.nil? || pref.to_s.strip.empty?
39
- return nil if city.nil? || city.to_s.strip.empty?
38
+ return nil if pref.to_s.strip.empty?
39
+ return nil if city.to_s.strip.empty?
40
40
 
41
41
  relation = postal_code_model.where(pref: pref, city: city)
42
- return relation if town.blank?
42
+ return relation if town.to_s.strip.empty?
43
43
 
44
44
  pattern = "#{escape_like(town.to_s.strip)}%"
45
45
  relation.where('town LIKE ?', pattern)
@@ -5,6 +5,7 @@
5
5
  require 'csv'
6
6
  require_relative 'postal_code_repository'
7
7
  require_relative '../address_record'
8
+ require_relative '../ken_all'
8
9
 
9
10
  module JpAddressComplement
10
11
  module Repositories
@@ -23,18 +24,7 @@ module JpAddressComplement
23
24
  # end
24
25
  #
25
26
  class CsvPostalCodeRepository < PostalCodeRepository
26
- # 列インデックス(KEN_ALL.CSV 形式)
27
- COL_PREF_CODE = 0 # : Integer
28
- COL_POSTAL_CODE = 2 # : Integer
29
- COL_KANA_PREF = 3 # : Integer
30
- COL_KANA_CITY = 4 # : Integer
31
- COL_KANA_TOWN = 5 # : Integer
32
- COL_PREF = 6 # : Integer
33
- COL_CITY = 7 # : Integer
34
- COL_TOWN = 8 # : Integer
35
- COL_IS_PARTIAL = 9 # : Integer
36
- COL_HAS_ALIAS = 12 # : Integer
37
- COL_IS_LARGE_OFFICE = 13 # : Integer
27
+ include KenAll
38
28
 
39
29
  # @rbs (String csv_path) -> void
40
30
  # @param csv_path [String] 読み込む KEN_ALL 形式 UTF-8 CSV のパス
@@ -76,6 +76,7 @@ module JpAddressComplement
76
76
  full = base + town_part
77
77
 
78
78
  return true if address.include?(full)
79
+ return true if address == base # 町域なしの住所も許容する
79
80
  return false if town_part.empty?
80
81
 
81
82
  # 日本郵便の CSV は町域に「字」「大字」を含まない。市区町村の直後に「字」「大字」が付いた表記は省略可能なため無視する。
@@ -20,7 +20,8 @@ module JpAddressComplement
20
20
  postal_code = record.public_send(postal_code_field)
21
21
  address = record.public_send(address_field)
22
22
 
23
- return if postal_code.blank? || address.blank?
23
+ return if (postal_code.nil? || postal_code.to_s.strip.empty?) ||
24
+ (address.nil? || address.to_s.strip.empty?)
24
25
 
25
26
  return if JpAddressComplement.valid_combination?(postal_code, address)
26
27
 
@@ -2,5 +2,5 @@
2
2
  # rbs_inline: enabled
3
3
 
4
4
  module JpAddressComplement
5
- VERSION = '0.1.0' #: String
5
+ VERSION = '0.3.0' #: String
6
6
  end
@@ -9,8 +9,13 @@ require_relative 'jp_address_complement/repositories/postal_code_repository'
9
9
  require_relative 'jp_address_complement/searcher'
10
10
  require_relative 'jp_address_complement/prefecture'
11
11
 
12
- # Rails 環境でのみ Railtie をロード
13
- require_relative 'jp_address_complement/railtie' if defined?(Rails)
12
+ # Rails が利用可能な場合のみ Railtie をロード(Gemfile に指定されていても require 前だと defined?(Rails) が false になり得るため、require を試す)
13
+ begin
14
+ require 'rails'
15
+ require_relative 'jp_address_complement/railtie'
16
+ rescue LoadError
17
+ # Rails が利用できない場合は Railtie をスキップ
18
+ end
14
19
 
15
20
  module JpAddressComplement
16
21
  class Error < StandardError; end
@@ -31,10 +36,19 @@ module JpAddressComplement
31
36
  @configuration ||= Configuration.new
32
37
  end
33
38
 
34
- # PostalCode モデルの継承元。未設定時は ActiveRecord::Base。configuration.postal_code_model_base に委譲する。
39
+ # PostalCode モデルの継承元。未設定時は ActiveRecord::Base(activerecord gem が利用可能な場合)。
40
+ # Gemfile に指定されていても require 前だと defined? が false になり得るため、未ロード時は require を試す。
35
41
  # @rbs () -> Class
36
42
  def base_record_class
37
- configuration.postal_code_model_base || ActiveRecord::Base
43
+ base = configuration.postal_code_model_base
44
+ return base if base
45
+
46
+ require 'active_record'
47
+ ActiveRecord::Base
48
+ rescue LoadError => e
49
+ raise Error,
50
+ 'ActiveRecord is not available. Add gem "activerecord" to your Gemfile, or set ' \
51
+ "JpAddressComplement.configuration.postal_code_model_base. (#{e.message})"
38
52
  end
39
53
 
40
54
  # @rbs (Class) -> void
@@ -124,6 +138,10 @@ module JpAddressComplement
124
138
  require_relative 'jp_address_complement/repositories/active_record_postal_code_repository'
125
139
  require_relative 'jp_address_complement/models/postal_code'
126
140
  Repositories::ActiveRecordPostalCodeRepository.new
141
+ rescue LoadError => e
142
+ raise Error,
143
+ 'ActiveRecord is not loaded. Add gem "activerecord" to your Gemfile to use the default repository, ' \
144
+ "or set JpAddressComplement.configuration.repository to your own implementation. (#{e.message})"
127
145
  end
128
146
  end
129
147
  end
@@ -21,30 +21,16 @@ module JpAddressComplement
21
21
  # UTF-8 版 KEN_ALL(utf_ken_all.csv)を読み込み、jp_address_complement_postal_codes テーブルに upsert する
22
22
  # UTF-8 形式の CSV を前提として処理する
23
23
  class CsvImporter
24
- BATCH_SIZE: Integer
25
-
26
- # 列インデックス(KEN_ALL.CSV 形式)
27
- COL_PREF_CODE: Integer
28
-
29
- COL_POSTAL_CODE: Integer
30
-
31
- COL_KANA_PREF: Integer
32
-
33
- COL_KANA_CITY: Integer
34
-
35
- COL_KANA_TOWN: Integer
24
+ include KenAll
36
25
 
37
- COL_PREF: Integer
26
+ # バッチ削除・ユニークキーとして使うカラム群
27
+ KEY_COLUMNS: Array[Symbol]
38
28
 
39
- COL_CITY: Integer
29
+ # SQLite のデフォルト式木深度制限(1000)対策。
30
+ # reduce(:or) で N 件 OR 結合すると深さ ≈ N + 6 になるため、500 件に収める(深さ ≈ 506)。
31
+ DELETE_CHUNK_SIZE: Integer
40
32
 
41
- COL_TOWN: Integer
42
-
43
- COL_IS_PARTIAL: Integer
44
-
45
- COL_HAS_ALIAS: Integer
46
-
47
- COL_IS_LARGE_OFFICE: Integer
33
+ BATCH_SIZE: Integer
48
34
 
49
35
  # @rbs (String csv_path) -> void
50
36
  def initialize: (String csv_path) -> void
@@ -66,6 +52,11 @@ module JpAddressComplement
66
52
  # @rbs (Array[Hash[Symbol, untyped]] batch) -> void
67
53
  def upsert_batch: (Array[Hash[Symbol, untyped]] batch) -> void
68
54
 
55
+ # バッチ内の全レコードを1クエリで一括削除する
56
+ # Arel を使うことで NULL カラム(town 等)を IS NULL として正しく扱う
57
+ # @rbs (Array[Hash[Symbol, untyped]] batch) -> void
58
+ def batch_delete: (Array[Hash[Symbol, untyped]] batch) -> void
59
+
69
60
  # 郵便番号・都道府県・市区町村・町域(漢字)が同じでも読み(カナ)が異なれば別レコードとして扱う
70
61
  # @rbs (Hash[Symbol, untyped] record) -> Array[String]
71
62
  def row_key: (Hash[Symbol, untyped] record) -> Array[String]
@@ -0,0 +1,33 @@
1
+ # Generated from lib/jp_address_complement/ken_all.rb with RBS::Inline
2
+
3
+ module JpAddressComplement
4
+ # UTF-8 版 KEN_ALL(utf_ken_all.csv)のフォーマット定数
5
+ #
6
+ # CSV の各列インデックスを定義する。include することでクラス内から直接参照できる。
7
+ #
8
+ # 参考: https://www.post.japanpost.jp/zipcode/dl/readme.html
9
+ module KenAll
10
+ # 列インデックス(KEN_ALL.CSV 形式)
11
+ COL_PREF_CODE: ::Integer
12
+
13
+ COL_POSTAL_CODE: ::Integer
14
+
15
+ COL_KANA_PREF: ::Integer
16
+
17
+ COL_KANA_CITY: ::Integer
18
+
19
+ COL_KANA_TOWN: ::Integer
20
+
21
+ COL_PREF: ::Integer
22
+
23
+ COL_CITY: ::Integer
24
+
25
+ COL_TOWN: ::Integer
26
+
27
+ COL_IS_PARTIAL: ::Integer
28
+
29
+ COL_HAS_ALIAS: ::Integer
30
+
31
+ COL_IS_LARGE_OFFICE: ::Integer
32
+ end
33
+ end
@@ -40,6 +40,11 @@ module JpAddressComplement
40
40
  # @rbs (String zip_path, String tmpdir) -> String?
41
41
  def extract_zip_entries: (String zip_path, String tmpdir) -> String?
42
42
 
43
+ # ZIP エントリの展開先絶対パスを安全に解決する。
44
+ # entry_name に含まれる ../ などにより tmpdir の外を指す場合は nil を返す(Zip Slip 対策)。
45
+ # @rbs (String tmpdir, String entry_name) -> String?
46
+ def safe_extract_dest: (String tmpdir, String entry_name) -> String?
47
+
43
48
  # @rbs (String entry_name, String dest) -> bool
44
49
  def entry_csv?: (String entry_name, String dest) -> bool
45
50
 
@@ -16,28 +16,7 @@ module JpAddressComplement
16
16
  # c.repository = JpAddressComplement::Repositories::CsvPostalCodeRepository.new('/path/to/utf_ken_all.csv')
17
17
  # end
18
18
  class CsvPostalCodeRepository < PostalCodeRepository
19
- # 列インデックス(KEN_ALL.CSV 形式)
20
- COL_PREF_CODE: ::Integer
21
-
22
- COL_POSTAL_CODE: ::Integer
23
-
24
- COL_KANA_PREF: ::Integer
25
-
26
- COL_KANA_CITY: ::Integer
27
-
28
- COL_KANA_TOWN: ::Integer
29
-
30
- COL_PREF: ::Integer
31
-
32
- COL_CITY: ::Integer
33
-
34
- COL_TOWN: ::Integer
35
-
36
- COL_IS_PARTIAL: ::Integer
37
-
38
- COL_HAS_ALIAS: ::Integer
39
-
40
- COL_IS_LARGE_OFFICE: ::Integer
19
+ include KenAll
41
20
 
42
21
  # @rbs (String csv_path) -> void
43
22
  # @param csv_path [String] 読み込む KEN_ALL 形式 UTF-8 CSV のパス
@@ -17,7 +17,8 @@ module JpAddressComplement
17
17
  # @return [Configuration]
18
18
  def self.configuration: () -> Configuration
19
19
 
20
- # PostalCode モデルの継承元。未設定時は ActiveRecord::Base。configuration.postal_code_model_base に委譲する。
20
+ # PostalCode モデルの継承元。未設定時は ActiveRecord::Base(activerecord gem が利用可能な場合)。
21
+ # Gemfile に指定されていても require 前だと defined? が false になり得るため、未ロード時は require を試す。
21
22
  # @rbs () -> Class
22
23
  def self.base_record_class: () -> Class
23
24
 
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jp_address_complement
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - naokirin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-02-28 00:00:00.000000000 Z
11
+ date: 2026-03-01 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: activerecord
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '7.0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '7.0'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: csv
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -39,33 +25,33 @@ dependencies:
39
25
  - !ruby/object:Gem::Version
40
26
  version: '3.0'
41
27
  - !ruby/object:Gem::Dependency
42
- name: railties
28
+ name: rubyzip
43
29
  requirement: !ruby/object:Gem::Requirement
44
30
  requirements:
45
31
  - - ">="
46
32
  - !ruby/object:Gem::Version
47
- version: '7.0'
33
+ version: '2.3'
48
34
  type: :runtime
49
35
  prerelease: false
50
36
  version_requirements: !ruby/object:Gem::Requirement
51
37
  requirements:
52
38
  - - ">="
53
39
  - !ruby/object:Gem::Version
54
- version: '7.0'
40
+ version: '2.3'
55
41
  - !ruby/object:Gem::Dependency
56
- name: rubyzip
42
+ name: activerecord
57
43
  requirement: !ruby/object:Gem::Requirement
58
44
  requirements:
59
45
  - - ">="
60
46
  - !ruby/object:Gem::Version
61
- version: '2.3'
62
- type: :runtime
47
+ version: '7.0'
48
+ type: :development
63
49
  prerelease: false
64
50
  version_requirements: !ruby/object:Gem::Requirement
65
51
  requirements:
66
52
  - - ">="
67
53
  - !ruby/object:Gem::Version
68
- version: '2.3'
54
+ version: '7.0'
69
55
  - !ruby/object:Gem::Dependency
70
56
  name: generator_spec
71
57
  requirement: !ruby/object:Gem::Requirement
@@ -108,6 +94,20 @@ dependencies:
108
94
  - - ">="
109
95
  - !ruby/object:Gem::Version
110
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: railties
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '7.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '7.0'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: rake
113
113
  requirement: !ruby/object:Gem::Requirement
@@ -355,6 +355,7 @@ files:
355
355
  - lib/jp_address_complement/address_record.rb
356
356
  - lib/jp_address_complement/configuration.rb
357
357
  - lib/jp_address_complement/importers/csv_importer.rb
358
+ - lib/jp_address_complement/ken_all.rb
358
359
  - lib/jp_address_complement/ken_all_downloader.rb
359
360
  - lib/jp_address_complement/models/postal_code.rb
360
361
  - lib/jp_address_complement/normalizer.rb
@@ -373,6 +374,7 @@ files:
373
374
  - sig/generated/jp_address_complement.rbs
374
375
  - sig/generated/jp_address_complement/configuration.rbs
375
376
  - sig/generated/jp_address_complement/importers/csv_importer.rbs
377
+ - sig/generated/jp_address_complement/ken_all.rbs
376
378
  - sig/generated/jp_address_complement/ken_all_downloader.rbs
377
379
  - sig/generated/jp_address_complement/normalizer.rbs
378
380
  - sig/generated/jp_address_complement/prefecture.rbs