sahih-al-bukhari 3.1.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e6ebc0c85847f52e123dc4894cef9e8d0a139a3659b1c57f77e85a90fbc3b57e
4
+ data.tar.gz: 5eac6a24cc055bff4f09ce2a866e8ddf855a7c6fecb6dfd0bfcab92765246cee
5
+ SHA512:
6
+ metadata.gz: 0e883151bc93ba133e9ae2fbf1c097e5174acbecd1ef6470eb2cf85aef9071bbca0e926339c8d070076b16df5e8576169de788d6ca097aebb5f622dfcd104a5d
7
+ data.tar.gz: 15eec75b751bbc3d093421cedeb7662fc796fd4a28a0826bf12b809a7a7855507050cd842602ddcfc24d5ebd511689410c01e85784e8b151dfaddf084fc5d4bf
data/README.md ADDED
@@ -0,0 +1,196 @@
1
+ # Sahih al-Bukhari Ruby Gem
2
+
3
+ Complete Sahih al-Bukhari collection for Ruby — 7,277 authentic hadiths with full Arabic text and English translations.
4
+
5
+ ## Features
6
+
7
+ - **Offline-first**: All data is bundled, no internet required
8
+ - **Zero dependencies**: Pure Ruby implementation
9
+ - **Complete collection**: All 7,277 hadiths from Sahih al-Bukhari
10
+ - **Bilingual**: Arabic text with English translations
11
+ - **CLI tool**: Command-line interface for quick access
12
+ - **Ruby library**: Easy integration into Ruby applications
13
+ - **Search functionality**: Find hadiths by text search
14
+ - **Chapter organization**: Browse by traditional chapter structure
15
+
16
+ ## Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ ```ruby
21
+ gem 'sahih-al-bukhari'
22
+ ```
23
+
24
+ And then execute:
25
+
26
+ ```bash
27
+ $ bundle install
28
+ ```
29
+
30
+ Or install it yourself as:
31
+
32
+ ```bash
33
+ $ gem install sahih-al-bukhari
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ### Ruby Library
39
+
40
+ ```ruby
41
+ require 'sahih_al_bukhari'
42
+
43
+ # Initialize the collection
44
+ bukhari = SahihAlBukhari::Bukhari.new
45
+
46
+ # Get a specific hadith
47
+ hadith = bukhari.get(1)
48
+ puts hadith.arabic
49
+ puts hadith.english
50
+
51
+ # Search for hadiths
52
+ results = bukhari.search("prayer")
53
+ results.each { |h| puts h.english }
54
+
55
+ # Get random hadith
56
+ random = bukhari.get_random
57
+ puts random.english
58
+
59
+ # Get hadiths by chapter
60
+ chapter_hadiths = bukhari.get_by_chapter(1)
61
+
62
+ # Browse chapters
63
+ bukhari.chapters.each do |chapter|
64
+ puts "Chapter #{chapter.id}: #{chapter.title}"
65
+ end
66
+ ```
67
+
68
+ ### Command Line Interface
69
+
70
+ ```bash
71
+ # Get a specific hadith
72
+ bukhari -n 1
73
+
74
+ # Search for hadiths
75
+ bukhari -s prayer
76
+
77
+ # Get random hadith
78
+ bukhari -r
79
+
80
+ # List all chapters
81
+ bukhari -l
82
+
83
+ # Get hadiths from a specific chapter
84
+ bukhari -c 1
85
+
86
+ # Show metadata
87
+ bukhari -m
88
+
89
+ # Output as JSON
90
+ bukhari -n 1 --json
91
+
92
+ # Limit search results
93
+ bukhari -s prayer --limit 5
94
+ ```
95
+
96
+ ## API Reference
97
+
98
+ ### SahihAlBukhari::Bukhari
99
+
100
+ #### Constructor
101
+
102
+ ```ruby
103
+ bukhari = SahihAlBukhari::Bukhari.new(data_path: optional_custom_path)
104
+ ```
105
+
106
+ #### Methods
107
+
108
+ - `get(number)` - Get hadith by number (1-based)
109
+ - `get_by_chapter(chapter_id)` - Get all hadiths from a chapter
110
+ - `search(query)` - Search hadiths by Arabic or English text
111
+ - `get_random()` - Get a random hadith
112
+ - `chapters` - Array of all chapters
113
+ - `metadata` - Collection metadata
114
+ - `length` - Total number of hadiths
115
+
116
+ #### Data Classes
117
+
118
+ **Hadith**
119
+ - `number` - Hadith number
120
+ - `arabic` - Arabic text
121
+ - `english` - English translation
122
+ - `chapter_id` - Chapter ID
123
+
124
+ **Chapter**
125
+ - `id` - Chapter ID
126
+ - `title` - English title
127
+ - `arabic_title` - Arabic title
128
+
129
+ **Metadata**
130
+ - `total_hadiths` - Total number of hadiths
131
+ - `total_chapters` - Total number of chapters
132
+ - `language` - Language codes
133
+ - `source` - Data source information
134
+
135
+ ## CLI Options
136
+
137
+ ```
138
+ Usage: bukhari [options]
139
+
140
+ -n, --number NUMBER Get specific hadith by number
141
+ -c, --chapter CHAPTER Get hadiths from specific chapter
142
+ -s, --search QUERY Search hadiths
143
+ -r, --random Get random hadith
144
+ -l, --list-chapters List all chapters
145
+ -m, --metadata Show collection metadata
146
+ -j, --json Output as JSON
147
+ --chapter-info Show chapter info with hadiths
148
+ --limit LIMIT Limit search results (default: 10)
149
+ -v, --version Show version
150
+ -h, --help Show this help
151
+ ```
152
+
153
+ ## Data Source
154
+
155
+ This gem shares the same data files as the Node.js and Python packages in this repository:
156
+
157
+ - `bin/bukhari.json` - Main data file (shared across all packages)
158
+ - `chapters/` - Chapter metadata files
159
+
160
+ The data is automatically detected in the following priority order:
161
+
162
+ 1. Custom `data_path` parameter
163
+ 2. `bin/bukhari.json` at repository root
164
+ 3. Bundled data in the gem
165
+ 4. CDN fallback (downloads automatically)
166
+
167
+ ## Development
168
+
169
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
170
+
171
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
172
+
173
+ To install this gem onto your local machine, run `bundle exec rake install`.
174
+
175
+ ### Rake Tasks
176
+
177
+ ```bash
178
+ rake spec # Run tests
179
+ rake yard # Generate documentation
180
+ rake bukhari:test_basic # Test basic functionality
181
+ rake bukhari:test_cli # Test CLI
182
+ rake bukhari:prepare_data # Download data files
183
+ ```
184
+
185
+ ## Contributing
186
+
187
+ Bug reports and pull requests are welcome on GitHub at https://github.com/SENODROOM/sahih-al-bukhari.
188
+
189
+ ## License
190
+
191
+ This gem is available as open source under the terms of the AGPL-3.0 License.
192
+
193
+ ## Version History
194
+
195
+ - **3.1.2** - Initial Ruby implementation
196
+ - Matches API compatibility with Python and Node.js versions
data/bin/bukhari ADDED
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # CLI for sahih-al-bukhari (Ruby package)
5
+
6
+ require 'optparse'
7
+ require 'json'
8
+ require_relative '../sahih_al_bukhari'
9
+
10
+ VERSION = SahihAlBukhari::VERSION
11
+
12
+ # ANSI color codes
13
+ module Colors
14
+ RESET = "\e[0m"
15
+ BOLD = "\e[1m"
16
+ DIM = "\e[2m"
17
+ GREEN = "\e[32m"
18
+ YELLOW = "\e[33m"
19
+ CYAN = "\e[36m"
20
+ MAGENTA = "\e[35m"
21
+ BLUE = "\e[34m"
22
+ RED = "\e[31m"
23
+ GRAY = "\e[90m"
24
+
25
+ def self.color(code, text)
26
+ "#{code}#{text}#{RESET}"
27
+ end
28
+
29
+ def self.bold(text); color(BOLD, text); end
30
+ def self.green(text); color(GREEN, text); end
31
+ def self.yellow(text); color(YELLOW, text); end
32
+ def self.cyan(text); color(CYAN, text); end
33
+ def self.magenta(text); color(MAGENTA, text); end
34
+ def self.blue(text); color(BLUE, text); end
35
+ def self.red(text); color(RED, text); end
36
+ def self.gray(text); color(GRAY, text); end
37
+ def self.dim(text); color(DIM, text); end
38
+ end
39
+
40
+ DIV = Colors.gray('─' * 60)
41
+
42
+ def format_hadith(hadith, show_chapter: false)
43
+ output = []
44
+ output << Colors.bold("Hadith #{hadith.number}")
45
+
46
+ if show_chapter
47
+ chapter = bukhari.chapters.find { |c| c.id == hadith.chapter_id }
48
+ output << Colors.cyan("Chapter #{chapter.id}: #{chapter.title}") if chapter
49
+ end
50
+
51
+ output << ""
52
+ output << Colors.bold("Arabic:")
53
+ output << hadith.arabic
54
+ output << ""
55
+ output << Colors.bold("English:")
56
+ output << hadith.english
57
+
58
+ output.join("\n")
59
+ end
60
+
61
+ def format_chapter(chapter)
62
+ output = []
63
+ output << Colors.bold("Chapter #{chapter.id}: #{chapter.title}")
64
+ output << Colors.dim(chapter.arabic_title)
65
+ output.join("\n")
66
+ end
67
+
68
+ def print_hadith(hadith, show_chapter: false)
69
+ puts DIV
70
+ puts format_hadith(hadith, show_chapter: show_chapter)
71
+ puts DIV
72
+ puts
73
+ end
74
+
75
+ def print_chapter(chapter)
76
+ puts DIV
77
+ puts format_chapter(chapter)
78
+ puts DIV
79
+ puts
80
+ end
81
+
82
+ def print_list(items, title)
83
+ puts Colors.bold("#{title} (#{items.length}):")
84
+ puts
85
+ items.each { |item| puts " #{item}" }
86
+ puts
87
+ end
88
+
89
+ # Parse command line arguments
90
+ options = {
91
+ number: nil,
92
+ chapter: nil,
93
+ search: nil,
94
+ random: false,
95
+ list_chapters: false,
96
+ metadata: false,
97
+ json: false,
98
+ chapter_info: false,
99
+ limit: 10
100
+ }
101
+
102
+ parser = OptionParser.new do |opts|
103
+ opts.banner = "Usage: bukhari [options]"
104
+
105
+ opts.on("-n", "--number NUMBER", Integer, "Get specific hadith by number") do |n|
106
+ options[:number] = n
107
+ end
108
+
109
+ opts.on("-c", "--chapter CHAPTER", Integer, "Get hadiths from specific chapter") do |c|
110
+ options[:chapter] = c
111
+ end
112
+
113
+ opts.on("-s", "--search QUERY", "Search hadiths") do |q|
114
+ options[:search] = q
115
+ end
116
+
117
+ opts.on("-r", "--random", "Get random hadith") do
118
+ options[:random] = true
119
+ end
120
+
121
+ opts.on("-l", "--list-chapters", "List all chapters") do
122
+ options[:list_chapters] = true
123
+ end
124
+
125
+ opts.on("-m", "--metadata", "Show collection metadata") do
126
+ options[:metadata] = true
127
+ end
128
+
129
+ opts.on("-j", "--json", "Output as JSON") do
130
+ options[:json] = true
131
+ end
132
+
133
+ opts.on("--chapter-info", "Show chapter info with hadiths") do
134
+ options[:chapter_info] = true
135
+ end
136
+
137
+ opts.on("--limit LIMIT", Integer, "Limit search results (default: 10)") do |l|
138
+ options[:limit] = l
139
+ end
140
+
141
+ opts.on("-v", "--version", "Show version") do
142
+ puts "bukhari #{VERSION}"
143
+ exit
144
+ end
145
+
146
+ opts.on("-h", "--help", "Show this help") do
147
+ puts opts
148
+ puts
149
+ puts Colors.bold("Examples:")
150
+ puts " bukhari -n 1 # Get hadith #1"
151
+ puts " bukhari -c 1 # Get hadiths from chapter 1"
152
+ puts " bukhari -s prayer # Search for 'prayer'"
153
+ puts " bukhari -r # Get random hadith"
154
+ puts " bukhari -l # List chapters"
155
+ puts " bukhari -m # Show metadata"
156
+ puts " bukhari -s prayer --limit 5 # Search with limit"
157
+ puts " bukhari -n 1 --json # Output as JSON"
158
+ exit
159
+ end
160
+ end
161
+
162
+ begin
163
+ parser.parse!
164
+ rescue OptionParser::InvalidOption => e
165
+ puts Colors.red("Error: #{e.message}")
166
+ puts parser
167
+ exit 1
168
+ end
169
+
170
+ # Initialize Bukhari instance
171
+ begin
172
+ $bukhari = SahihAlBukhari::Bukhari.new
173
+ rescue SahihAlBukhari::Error => e
174
+ puts Colors.red("Error: #{e.message}")
175
+ exit 1
176
+ end
177
+
178
+ # Execute commands
179
+ if options[:number]
180
+ begin
181
+ hadith = $bukhari.get(options[:number])
182
+ if options[:json]
183
+ puts hadith.to_json
184
+ else
185
+ print_hadith(hadith, show_chapter: options[:chapter_info])
186
+ end
187
+ rescue SahihAlBukhari::HadithNotFoundError => e
188
+ puts Colors.red("Error: #{e.message}")
189
+ exit 1
190
+ end
191
+ elsif options[:chapter]
192
+ begin
193
+ hadiths = $bukhari.get_by_chapter(options[:chapter])
194
+ if options[:json]
195
+ puts hadiths.map(&:to_h).to_json
196
+ else
197
+ chapter = $bukhari.chapters.find { |c| c.id == options[:chapter] }
198
+ if chapter
199
+ print_chapter(chapter)
200
+ puts Colors.bold("Hadiths in this chapter (#{hadiths.length}):")
201
+ puts
202
+ end
203
+
204
+ hadiths.each { |h| print_hadith(h) }
205
+ end
206
+ rescue SahihAlBukhari::ChapterNotFoundError => e
207
+ puts Colors.red("Error: #{e.message}")
208
+ exit 1
209
+ end
210
+ elsif options[:search]
211
+ results = $bukhari.search(options[:search])
212
+ limited_results = results.first(options[:limit])
213
+
214
+ if options[:json]
215
+ puts limited_results.map(&:to_h).to_json
216
+ else
217
+ if results.empty?
218
+ puts Colors.yellow("No hadiths found for '#{options[:search]}'")
219
+ else
220
+ puts Colors.bold("Found #{results.length} hadith(s) matching '#{options[:search]}'")
221
+ puts Colors.dim("Showing first #{limited_results.length}:") if results.length > options[:limit]
222
+ puts
223
+
224
+ limited_results.each { |h| print_hadith(h) }
225
+ end
226
+ end
227
+ elsif options[:random]
228
+ hadith = $bukhari.get_random
229
+ if options[:json]
230
+ puts hadith.to_json
231
+ else
232
+ print_hadith(hadith, show_chapter: options[:chapter_info])
233
+ end
234
+ elsif options[:list_chapters]
235
+ if options[:json]
236
+ puts $bukhari.chapters.map(&:to_h).to_json
237
+ else
238
+ puts Colors.bold("Chapters in Sahih al-Bukhari:")
239
+ puts
240
+ $bukhari.chapters.each { |c| print_chapter(c) }
241
+ end
242
+ elsif options[:metadata]
243
+ if options[:json]
244
+ puts $bukhari.metadata.to_h.to_json
245
+ else
246
+ puts Colors.bold("Sahih al-Bukhari Collection Metadata:")
247
+ puts
248
+ metadata = $bukhari.metadata
249
+ puts " Total Hadiths: #{Colors.green(metadata.total_hadiths.to_s)}"
250
+ puts " Total Chapters: #{Colors.green(metadata.total_chapters.to_s)}"
251
+ puts " Language: #{Colors.cyan(metadata.language)}"
252
+ puts " Source: #{Colors.dim(metadata.source)}"
253
+ puts
254
+ end
255
+ else
256
+ # Default: show help
257
+ puts parser
258
+ puts Colors.bold("Quick start:")
259
+ puts " bukhari -r # Get a random hadith"
260
+ puts " bukhari -s prayer # Search for hadiths about prayer"
261
+ puts " bukhari -l # List all chapters"
262
+ end
Binary file
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ # sahih-al-bukhari — Complete Sahih al-Bukhari for Ruby.
4
+ #
5
+ # Quick start:
6
+ # require 'sahih_al_bukhari'
7
+ #
8
+ # bukhari = SahihAlBukhari::Bukhari.new
9
+ # bukhari.get(1)
10
+ # bukhari.search("prayer")
11
+ # bukhari.get_random
12
+ # bukhari.get_by_chapter(1)
13
+
14
+ require 'json'
15
+ require 'zlib'
16
+ require 'net/http'
17
+ require 'uri'
18
+ require 'fileutils'
19
+ require 'tmpdir'
20
+
21
+ require_relative 'version'
22
+
23
+ module SahihAlBukhari
24
+ CDN_URL = "https://cdn.jsdelivr.net/npm/sahih-al-bukhari@#{VERSION}/data/bukhari.json.gz"
25
+
26
+ # Locate the shared data file relative to the gem root.
27
+ # Layout (installed gem): lib/sahih_al_bukhari/bukhari.rb
28
+ # data/bukhari.json.gz ← same gem root
29
+ # Layout (monorepo dev): ruby/lib/sahih_al_bukhari/bukhari.rb
30
+ # data/bukhari.json.gz ← repo root
31
+ _LIB_DIR = File.expand_path('..', __dir__) # …/ruby/lib
32
+ _GEM_ROOT = File.expand_path('..', _LIB_DIR) # …/ruby (installed: gem root)
33
+ _REPO_ROOT = File.expand_path('..', _GEM_ROOT) # …/ (monorepo root)
34
+
35
+ INSTALLED_DATA = File.join(_GEM_ROOT, 'data', 'bukhari.json.gz')
36
+ REPO_DATA_GZ = File.join(_REPO_ROOT, 'data', 'bukhari.json.gz')
37
+ REPO_DATA_JSON = File.join(_REPO_ROOT, 'data', 'bukhari.json')
38
+
39
+ # ---------------------------------------------------------------------------
40
+ class Error < StandardError; end
41
+ class HadithNotFoundError < Error; end
42
+ class ChapterNotFoundError < Error; end
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Data structures
46
+ # ---------------------------------------------------------------------------
47
+
48
+ class Hadith
49
+ # FIX: JSON uses keys "id" and "chapterId", NOT "number" / "chapter_id".
50
+ # All accesses in the original code used the wrong key names, so every
51
+ # hadith came back with nil fields.
52
+ attr_reader :id, :number, :arabic, :english, :chapter_id
53
+
54
+ def initialize(data)
55
+ @id = data['id']
56
+ @number = data['id'] # alias kept for backward compat
57
+ @chapter_id = data['chapterId']
58
+ @arabic = data['arabic'] || ''
59
+ _en = data['english'] || {}
60
+ # english field is a Hash {"narrator"=>..., "text"=>...}
61
+ @english = _en
62
+ @narrator = _en['narrator'] || ''
63
+ @text = _en['text'] || ''
64
+ end
65
+
66
+ def narrator = @narrator
67
+ def text = @text
68
+
69
+ def to_h
70
+ {
71
+ id: @id,
72
+ number: @number,
73
+ arabic: @arabic,
74
+ english: @english,
75
+ chapter_id: @chapter_id
76
+ }
77
+ end
78
+
79
+ def to_json(*args) = to_h.to_json(*args)
80
+
81
+ def inspect
82
+ preview = @text.length > 60 ? "#{@text[0, 60]}…" : @text
83
+ "#<Hadith id=#{@id} chapterId=#{@chapter_id} text=#{preview.inspect}>"
84
+ end
85
+ end
86
+
87
+ # ---------------------------------------------------------------------------
88
+
89
+ class Chapter
90
+ # FIX: JSON uses keys "english" and "arabic" for the title strings.
91
+ # Original code passed "title" / "arabic_title" which don't exist in the data.
92
+ attr_reader :id, :title, :arabic_title
93
+
94
+ def initialize(data)
95
+ @id = data['id']
96
+ @title = data['english'] || '' # FIX: key is "english", not "title"
97
+ @arabic_title = data['arabic'] || '' # FIX: key is "arabic", not "arabic_title"
98
+ end
99
+
100
+ def to_h
101
+ { id: @id, title: @title, arabic_title: @arabic_title }
102
+ end
103
+
104
+ def to_json(*args) = to_h.to_json(*args)
105
+ def inspect = "#<Chapter id=#{@id} title=#{@title.inspect}>"
106
+ end
107
+
108
+ # ---------------------------------------------------------------------------
109
+
110
+ class Metadata
111
+ # FIX: the actual JSON has a completely different metadata structure:
112
+ # { "id": 1, "length": 7277, "arabic": {...}, "english": {...} }
113
+ # The original code expected flat keys like "total_hadiths", "language",
114
+ # "source" — none of which exist — so metadata was always nil/broken.
115
+ attr_reader :total_hadiths, :total_chapters, :language, :source,
116
+ :arabic, :english
117
+
118
+ def initialize(data, total_chapters)
119
+ @total_hadiths = data['length'] || 0
120
+ @total_chapters = total_chapters
121
+ @arabic = data['arabic'] || {}
122
+ @english = data['english'] || {}
123
+ @language = 'Arabic / English'
124
+ @source = @english['title'] || 'Sahih al-Bukhari'
125
+ end
126
+
127
+ def to_h
128
+ {
129
+ total_hadiths: @total_hadiths,
130
+ total_chapters: @total_chapters,
131
+ language: @language,
132
+ source: @source,
133
+ arabic: @arabic,
134
+ english: @english
135
+ }
136
+ end
137
+
138
+ def to_json(*args) = to_h.to_json(*args)
139
+ end
140
+
141
+ # ---------------------------------------------------------------------------
142
+ # Main Bukhari class
143
+ # ---------------------------------------------------------------------------
144
+
145
+ class Bukhari
146
+ include Enumerable
147
+
148
+ attr_reader :metadata, :chapters, :length
149
+
150
+ def initialize(data_path: nil)
151
+ path = data_path || find_data_file
152
+ raw = load_raw(path)
153
+
154
+ @chapters = raw['chapters'].map { |c| Chapter.new(c) }
155
+ @metadata = Metadata.new(raw['metadata'], @chapters.length)
156
+ # Build Hadith objects and an O(1) lookup map
157
+ @hadiths = raw['hadiths'].map { |h| Hadith.new(h) }
158
+ @by_id = @hadiths.each_with_object({}) { |h, m| m[h.id] = h }
159
+ @length = @hadiths.length
160
+ end
161
+
162
+ # ── Public API ────────────────────────────────────────────────────────────
163
+
164
+ # Get a hadith by its global id (1-based, matches the JSON "id" field).
165
+ def get(id)
166
+ @by_id[id] || raise(HadithNotFoundError, "Hadith #{id} not found")
167
+ end
168
+
169
+ # All hadiths belonging to chapter_id.
170
+ def get_by_chapter(chapter_id)
171
+ unless @chapters.any? { |c| c.id == chapter_id }
172
+ raise ChapterNotFoundError, "Chapter #{chapter_id} not found"
173
+ end
174
+ @hadiths.select { |h| h.chapter_id == chapter_id }
175
+ end
176
+
177
+ # Full-text search across English text + narrator. Case-insensitive.
178
+ # FIX: original called String#downcase on the `english` field which is a
179
+ # Hash, not a String — that raised NoMethodError at runtime.
180
+ def search(query)
181
+ q = query.downcase
182
+ @hadiths.select do |h|
183
+ h.text.downcase.include?(q) || h.narrator.downcase.include?(q) ||
184
+ h.arabic.downcase.include?(q)
185
+ end
186
+ end
187
+
188
+ def get_random
189
+ @hadiths.sample
190
+ end
191
+
192
+ # Array-like index access (0-based).
193
+ def [](index)
194
+ @hadiths[index]
195
+ end
196
+
197
+ # Enumerable — lets callers use .map, .select, .each, .first, etc.
198
+ def each(&block)
199
+ @hadiths.each(&block)
200
+ end
201
+
202
+ def to_a
203
+ @hadiths.dup
204
+ end
205
+
206
+ # ── Private helpers ───────────────────────────────────────────────────────
207
+ private
208
+
209
+ def find_data_file
210
+ candidates = [INSTALLED_DATA, REPO_DATA_GZ, REPO_DATA_JSON]
211
+ candidates.find { |p| File.exist?(p) } || download_from_cdn
212
+ end
213
+
214
+ def load_raw(path)
215
+ data = if path.end_with?('.gz')
216
+ Zlib::GzipReader.open(path) { |gz| gz.read }
217
+ else
218
+ File.read(path, encoding: 'utf-8')
219
+ end
220
+ JSON.parse(data)
221
+ rescue => e
222
+ raise Error, "Failed to load data from #{path}: #{e.message}"
223
+ end
224
+
225
+ def download_from_cdn
226
+ # FIX: original hard-coded 'C:\temp' — use Ruby's cross-platform tmpdir
227
+ dest = File.join(Dir.tmpdir, 'bukhari.json.gz')
228
+ return dest if File.exist?(dest)
229
+
230
+ warn "[sahih-al-bukhari] Downloading data from CDN (one-time) …"
231
+ uri = URI(CDN_URL)
232
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
233
+ resp = http.get(uri.request_uri)
234
+ raise Error, "CDN returned HTTP #{resp.code}" unless resp.is_a?(Net::HTTPSuccess)
235
+ File.binwrite(dest, resp.body)
236
+ end
237
+ dest
238
+ rescue => e
239
+ raise Error, "Failed to download data from CDN: #{e.message}"
240
+ end
241
+ end
242
+
243
+ # Clear any cached data (no-op in current implementation, kept for API compat)
244
+ def self.clear_cache; end
245
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SahihAlBukhari
4
+ # FIX: gemspec referenced SahihAlBukhari::Version::VERSION (a nested module that
5
+ # never existed). The constant must live directly in SahihAlBukhari so that both
6
+ # the gemspec and sahih_al_bukhari.rb agree on its location.
7
+ VERSION = '3.1.2'
8
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # FIX: the original file contained only:
4
+ # require_relative '../sahih_al_bukhari'
5
+ # which tried to load ruby/sahih_al_bukhari.rb via a relative path that breaks
6
+ # once the gem is installed (the installed gem's require_paths is ['lib'], so
7
+ # `require 'sahih_al_bukhari'` resolves to THIS file, not the root-level one).
8
+ #
9
+ # The correct approach: this IS the library entry point. Load version first,
10
+ # then the main implementation file that lives alongside it.
11
+
12
+ require_relative 'sahih_al_bukhari/version'
13
+ require_relative 'sahih_al_bukhari/bukhari'
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/sahih_al_bukhari/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'sahih-al-bukhari'
7
+ spec.version = SahihAlBukhari::VERSION # FIX: was SahihAlBukhari::Version::VERSION (wrong namespace)
8
+ spec.authors = ['muhammadsaadamin']
9
+ spec.email = ['muhammadsaadamin@example.com']
10
+
11
+ spec.summary = 'Complete Sahih al-Bukhari — 7,277 hadiths. Offline-first, zero dependencies. CLI + Ruby library.'
12
+ spec.description = 'Complete Sahih al-Bukhari collection with 7,277 authentic hadiths featuring full Arabic text and English translations. Offline-first with zero external dependencies. Includes both Ruby library and command-line interface.'
13
+ spec.homepage = 'https://github.com/SENODROOM/sahih-al-bukhari'
14
+ spec.license = 'AGPL-3.0'
15
+ spec.required_ruby_version = '>= 2.7.0'
16
+
17
+ # FIX: spec.metadata must be assigned ONCE — previous code assigned it twice,
18
+ # which silently overwrote the first assignment (losing allowed_push_host etc.)
19
+ spec.metadata = {
20
+ 'allowed_push_host' => 'https://rubygems.org',
21
+ 'homepage_uri' => 'https://github.com/SENODROOM/sahih-al-bukhari',
22
+ 'source_code_uri' => 'https://github.com/SENODROOM/sahih-al-bukhari',
23
+ 'changelog_uri' => 'https://github.com/SENODROOM/sahih-al-bukhari/blob/main/CHANGELOG.md',
24
+ 'bug_tracker_uri' => 'https://github.com/SENODROOM/sahih-al-bukhari/issues',
25
+ 'rubygems_mfa_required' => 'true'
26
+ }
27
+
28
+ # FIX: `git ls-files` fails when building outside a git worktree (e.g. CI unshallow
29
+ # clone, or after `gem build` in a temp directory). Use Dir.glob instead so the
30
+ # file list is always reliable.
31
+ spec.files = Dir.chdir(__dir__) do
32
+ Dir.glob('{bin,lib,data}/**/*', File::FNM_DOTMATCH)
33
+ .reject { |f| File.directory?(f) }
34
+ .concat(%w[README.md LICENSE sahih_al_bukhari.gemspec])
35
+ .select { |f| File.exist?(f) }
36
+ end
37
+
38
+ spec.bindir = 'bin'
39
+ spec.executables = ['bukhari']
40
+ spec.require_paths = ['lib']
41
+
42
+ # Development dependencies
43
+ spec.add_development_dependency 'rake', '~> 13.0'
44
+ spec.add_development_dependency 'test-unit', '~> 3.6' # FIX: removed from Ruby 4.0 stdlib
45
+ spec.add_development_dependency 'rspec', '~> 3.0'
46
+ spec.add_development_dependency 'rubocop', '~> 1.21'
47
+ spec.add_development_dependency 'yard', '~> 0.9'
48
+
49
+ # FIX: spec.has_rdoc and spec.test_files are both deprecated and raise warnings
50
+ # on modern RubyGems — removed entirely.
51
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sahih-al-bukhari
3
+ version: !ruby/object:Gem::Version
4
+ version: 3.1.2
5
+ platform: ruby
6
+ authors:
7
+ - muhammadsaadamin
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rake
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '13.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '13.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: test-unit
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.6'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.6'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rubocop
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.21'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.21'
68
+ - !ruby/object:Gem::Dependency
69
+ name: yard
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '0.9'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '0.9'
82
+ description: Complete Sahih al-Bukhari collection with 7,277 authentic hadiths featuring
83
+ full Arabic text and English translations. Offline-first with zero external dependencies.
84
+ Includes both Ruby library and command-line interface.
85
+ email:
86
+ - muhammadsaadamin@example.com
87
+ executables:
88
+ - bukhari
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - README.md
93
+ - bin/bukhari
94
+ - data/bukhari.json.gz
95
+ - lib/sahih_al_bukhari.rb
96
+ - lib/sahih_al_bukhari/bukhari.rb
97
+ - lib/sahih_al_bukhari/version.rb
98
+ - sahih_al_bukhari.gemspec
99
+ homepage: https://github.com/SENODROOM/sahih-al-bukhari
100
+ licenses:
101
+ - AGPL-3.0
102
+ metadata:
103
+ allowed_push_host: https://rubygems.org
104
+ homepage_uri: https://github.com/SENODROOM/sahih-al-bukhari
105
+ source_code_uri: https://github.com/SENODROOM/sahih-al-bukhari
106
+ changelog_uri: https://github.com/SENODROOM/sahih-al-bukhari/blob/main/CHANGELOG.md
107
+ bug_tracker_uri: https://github.com/SENODROOM/sahih-al-bukhari/issues
108
+ rubygems_mfa_required: 'true'
109
+ rdoc_options: []
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: 2.7.0
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ requirements: []
123
+ rubygems_version: 4.0.6
124
+ specification_version: 4
125
+ summary: Complete Sahih al-Bukhari — 7,277 hadiths. Offline-first, zero dependencies.
126
+ CLI + Ruby library.
127
+ test_files: []