openfigi_ruby 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bb96df5aae03995901f25a8e042064c881d7d43bd2763d2164feca902588c797
4
+ data.tar.gz: b7d083008c394a57fd000480eabdf52348c7f207c1a17a7db9435112402a8aea
5
+ SHA512:
6
+ metadata.gz: a027a62e72d4f2c7e18f2da594fe15576f9fcc49d5ec1d443ccdaec26457fdd4924e548dade296e2ba701ce1d585ab98e733567436a3403a82d0549c9abeaae5
7
+ data.tar.gz: 0b147217f140a7bd5d810e148f474c4fa0c51bcd5841924234b331f19778179079f478aae1d1d28d9dfcb24f00f09487e0c40395e07ca4333c440e1754c3e624
data/.yardopts ADDED
@@ -0,0 +1,6 @@
1
+ --no-private
2
+ --markup markdown
3
+ --output-dir doc/
4
+ lib/**/*.rb
5
+ -
6
+ README.md
data/CLAUDE.md ADDED
@@ -0,0 +1,76 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ bin/setup # Install dependencies
9
+ bin/console # Open IRB session with the gem loaded (for manual testing)
10
+ bundle exec rake # Run default rake tasks
11
+ gem build # Build the .gem file
12
+ ```
13
+
14
+ No test framework is configured. If tests are added, update this file with the test command.
15
+
16
+ ## Architecture
17
+
18
+ A Ruby gem wrapping the [OpenFIGI V3 API](https://www.openfigi.com/api) — a financial data service for mapping identifiers (ISIN, CUSIP, ticker, etc.) to FIGIs (Financial Instrument Global Identifiers). V3 only; V2 is sunsetting.
19
+
20
+ **Base URL:** `https://api.openfigi.com/v3`
21
+ **Auth:** optional `X-OPENFIGI-APIKEY` header
22
+ **Rate limits:** 25 req/min (unauthenticated) · 25 req/6s (authenticated) for mapping; lower limits for search/filter
23
+
24
+ ### Module layout
25
+
26
+ ```
27
+ lib/openfigi_ruby.rb # Entry point: requires all sub-files, exposes .configure
28
+ lib/openfigi_ruby/
29
+ version.rb # VERSION constant
30
+ configuration.rb # Configuration (api_key, open_timeout, read_timeout)
31
+ error.rb # Error hierarchy (see below)
32
+ client.rb # HTTP client — all API methods live here
33
+ figi_result.rb # FigiResult Struct (one matched instrument)
34
+ mapping_result.rb # MappingResult Struct (data array or warning)
35
+ search_result.rb # SearchResult and FilterResult Structs
36
+ ```
37
+
38
+ ### Client API
39
+
40
+ ```ruby
41
+ OpenfigiRuby.configure { |c| c.api_key = ENV["OPENFIGI_API_KEY"] }
42
+ client = OpenfigiRuby::Client.new # uses global config; accepts api_key: override
43
+
44
+ client.mapping([{ id_type: "ID_ISIN", id_value: "US4592001014" }])
45
+ # => [MappingResult(data: [FigiResult(...)], warning: nil)]
46
+
47
+ client.mapping_values(:id_type)
48
+ # => ["ID_ISIN", "ID_CUSIP", ...]
49
+
50
+ client.search(query: "Apple", exch_code: "US")
51
+ # => SearchResult(data: [...], next_page: "...", error: nil)
52
+
53
+ client.filter(query: "Apple", currency: "USD")
54
+ # => FilterResult(data: [...], next_page: "...", total: 42, error: nil)
55
+ ```
56
+
57
+ All input hashes use **snake_case** symbol keys; the client converts to camelCase for the API. Response structs use snake_case accessors. `next_page` holds the pagination token (the API field is `next`).
58
+
59
+ ### Error hierarchy
60
+
61
+ ```
62
+ OpenfigiRuby::Error
63
+ └─ ApiError (status_code:, body: attributes)
64
+ ├─ AuthenticationError # HTTP 401
65
+ ├─ RateLimitError # HTTP 429
66
+ ├─ InvalidRequestError # HTTP 400
67
+ └─ ServerError # HTTP 500/503
68
+ ```
69
+
70
+ ### Conventions
71
+
72
+ - All files use `# frozen_string_literal: true`
73
+ - Ruby >= 3.1.0 required
74
+ - No runtime dependencies — uses only `net/http`, `json`, `uri` from stdlib
75
+ - Runtime dependencies go in `openfigi_ruby.gemspec`; dev-only in `Gemfile`
76
+ - Version is the single source of truth in `lib/openfigi_ruby/version.rb`
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Phong Si
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # OpenfigiRuby
2
+
3
+ Ruby client for the [OpenFIGI V3 API](https://www.openfigi.com/api). Maps financial identifiers (ISIN, CUSIP, ticker, etc.) to FIGIs (Financial Instrument Global Identifiers), with support for keyword search and filtering.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bundle add openfigi_ruby
9
+ ```
10
+
11
+ Or add to your Gemfile manually:
12
+
13
+ ```ruby
14
+ gem "openfigi_ruby"
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ### Configuration
20
+
21
+ ```ruby
22
+ OpenfigiRuby.configure do |config|
23
+ config.api_key = ENV["OPENFIGI_API_KEY"] # optional but recommended for higher rate limits
24
+ config.open_timeout = 10 # seconds (default: 10)
25
+ config.read_timeout = 30 # seconds (default: 30)
26
+ end
27
+
28
+ client = OpenfigiRuby::Client.new
29
+ ```
30
+
31
+ You can also pass an API key per-client without touching global config:
32
+
33
+ ```ruby
34
+ client = OpenfigiRuby::Client.new(api_key: "your_key")
35
+ ```
36
+
37
+ ---
38
+
39
+ ### Mapping identifiers to FIGIs
40
+
41
+ `mapping` accepts up to 10 jobs per request (100 with an API key) and returns one result per job.
42
+
43
+ ```ruby
44
+ results = client.mapping([
45
+ { id_type: "ID_ISIN", id_value: "US0378331005" },
46
+ { id_type: "ID_CUSIP", id_value: "037833100" },
47
+ { id_type: "TICKER", id_value: "AAPL", exch_code: "US" },
48
+ ])
49
+
50
+ results.each do |result|
51
+ if result.found?
52
+ result.data.each do |figi|
53
+ puts "#{figi.name} — #{figi.figi} (#{figi.exch_code})"
54
+ end
55
+ else
56
+ puts "No match: #{result.warning}"
57
+ end
58
+ end
59
+ ```
60
+
61
+ **Optional filters per job:**
62
+
63
+ | Key | Description |
64
+ |---|---|
65
+ | `:exch_code` | Exchange code (e.g. `"US"`, `"LN"`) |
66
+ | `:mic_code` | Market Identifier Code |
67
+ | `:currency` | Currency code (e.g. `"USD"`) |
68
+ | `:market_sec_des` | Market sector description |
69
+ | `:security_type` | Primary security type |
70
+ | `:security_type2` | Secondary security type (required for `BASE_TICKER`/`ID_EXCH_SYMBOL`) |
71
+ | `:include_unlisted_equities` | Boolean |
72
+ | `:option_type` | `"Put"` or `"Call"` |
73
+ | `:strike` | Two-element numeric range, e.g. `[100, 150]` |
74
+ | `:expiration` | Two-element date range, e.g. `["2024-01-01", "2024-12-31"]` |
75
+ | `:maturity` | Two-element date range (required for Pool) |
76
+ | `:state_code` | US state code |
77
+
78
+ ---
79
+
80
+ ### Fetching valid enum values
81
+
82
+ Look up the accepted values for any filterable field:
83
+
84
+ ```ruby
85
+ client.mapping_values(:id_type)
86
+ # => ["ID_ISIN", "ID_CUSIP", "TICKER", "ID_BB_GLOBAL", ...]
87
+
88
+ client.mapping_values(:exch_code)
89
+ # => ["US", "LN", "UN", ...]
90
+ ```
91
+
92
+ Valid keys: `:id_type`, `:exch_code`, `:mic_code`, `:currency`, `:market_sec_des`, `:security_type`, `:security_type2`
93
+
94
+ ---
95
+
96
+ ### Keyword search
97
+
98
+ Search for FIGIs by name or ticker. Results are paginated.
99
+
100
+ ```ruby
101
+ result = client.search(query: "Apple", exch_code: "US", security_type: "Common Stock")
102
+
103
+ result.data.each { |figi| puts "#{figi.ticker} — #{figi.figi}" }
104
+
105
+ # Fetch the next page
106
+ if result.next_page
107
+ result = client.search(query: "Apple", exch_code: "US", start: result.next_page)
108
+ end
109
+ ```
110
+
111
+ ---
112
+
113
+ ### Filter
114
+
115
+ Like search, but results are sorted alphabetically by FIGI and include a total count. Query is optional.
116
+
117
+ ```ruby
118
+ result = client.filter(currency: "USD", security_type: "Common Stock")
119
+
120
+ puts "#{result.total} total matches"
121
+ result.data.each { |figi| puts figi.figi }
122
+
123
+ # Paginate
124
+ while result.next_page
125
+ result = client.filter(currency: "USD", security_type: "Common Stock", start: result.next_page)
126
+ result.data.each { |figi| puts figi.figi }
127
+ end
128
+ ```
129
+
130
+ ---
131
+
132
+ ### Error handling
133
+
134
+ ```ruby
135
+ begin
136
+ results = client.mapping([{ id_type: "TICKER", id_value: "AAPL" }])
137
+ rescue OpenfigiRuby::AuthenticationError
138
+ # HTTP 401 — check your API key
139
+ rescue OpenfigiRuby::RateLimitError
140
+ # HTTP 429 — back off and retry
141
+ rescue OpenfigiRuby::InvalidRequestError => e
142
+ # HTTP 400 — malformed request
143
+ puts e.body
144
+ rescue OpenfigiRuby::ServerError
145
+ # HTTP 500/503 — retry with exponential backoff
146
+ rescue OpenfigiRuby::ApiError => e
147
+ # catch-all for any other non-200 response
148
+ puts "#{e.status_code}: #{e.message}"
149
+ end
150
+ ```
151
+
152
+ ---
153
+
154
+ ### Rate limits
155
+
156
+ | | Without API key | With API key |
157
+ |---|---|---|
158
+ | **Mapping** | 25 req/min, 10 jobs/req | 25 req/6s, 100 jobs/req |
159
+ | **Search/Filter** | 5 req/min | 20 req/min |
160
+
161
+ Get a free API key at [openfigi.com](https://www.openfigi.com/api#get-started).
162
+
163
+ ## Development
164
+
165
+ ```bash
166
+ bin/setup # install dependencies
167
+ bin/console # interactive prompt with the gem loaded
168
+ bundle exec rake # run tests
169
+ bundle exec yard server # browse docs at http://localhost:8808
170
+ ```
171
+
172
+ ## License
173
+
174
+ [MIT](LICENSE.txt)
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+ require "yard"
6
+
7
+ Rake::TestTask.new(:test) do |t|
8
+ t.libs << "test"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ YARD::Rake::YardocTask.new(:doc)
13
+
14
+ task default: :test
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module OpenfigiRuby
8
+ # HTTP client for the OpenFIGI V3 API.
9
+ #
10
+ # Instantiate with an optional API key override, or configure globally via
11
+ # {OpenfigiRuby.configure} and call +Client.new+ with no arguments.
12
+ class Client
13
+ # Valid field names accepted by {#mapping_values}.
14
+ # @api private
15
+ MAPPING_VALUE_KEYS = %w[idType exchCode micCode currency marketSecDes securityType securityType2].freeze
16
+
17
+ # @param api_key [String, nil] overrides the globally configured key
18
+ # @param open_timeout [Integer] seconds before the connection times out
19
+ # @param read_timeout [Integer] seconds before the read times out
20
+ def initialize(
21
+ api_key: OpenfigiRuby.configuration.api_key,
22
+ open_timeout: OpenfigiRuby.configuration.open_timeout,
23
+ read_timeout: OpenfigiRuby.configuration.read_timeout
24
+ )
25
+ @api_key = api_key
26
+ @open_timeout = open_timeout
27
+ @read_timeout = read_timeout
28
+ end
29
+
30
+ # Maps third-party identifiers to FIGIs (POST /v3/mapping).
31
+ #
32
+ # @param jobs [Array<Hash>] each hash must include +:id_type+ and +:id_value+.
33
+ # Optional keys: +:exch_code+, +:mic_code+, +:currency+, +:market_sec_des+,
34
+ # +:security_type+, +:security_type2+, +:include_unlisted_equities+,
35
+ # +:option_type+, +:strike+, +:contract_size+, +:coupon+,
36
+ # +:expiration+, +:maturity+, +:state_code+.
37
+ # @return [Array<MappingResult>] one result per input job
38
+ #
39
+ # @example
40
+ # client.mapping([
41
+ # { id_type: "ID_ISIN", id_value: "US4592001014" },
42
+ # { id_type: "TICKER", id_value: "AAPL", exch_code: "US" }
43
+ # ])
44
+ def mapping(jobs)
45
+ body = jobs.map { |job| serialize(job) }
46
+ response = post("/mapping", body)
47
+ response.map { |item| parse_mapping_result(item) }
48
+ end
49
+
50
+ # Returns the set of valid values for a filterable field (GET /v3/mapping/values/:key).
51
+ #
52
+ # @param key [String, Symbol] snake_case or camelCase field name.
53
+ # Valid values: :id_type, :exch_code, :mic_code, :currency,
54
+ # :market_sec_des, :security_type, :security_type2
55
+ # @return [Array<String>]
56
+ #
57
+ # @example
58
+ # client.mapping_values(:id_type)
59
+ def mapping_values(key)
60
+ api_key = camelize(key.to_s)
61
+ response = get("/mapping/values/#{api_key}")
62
+ response["values"]
63
+ end
64
+
65
+ # Searches for FIGIs by keyword (POST /v3/search).
66
+ #
67
+ # @param query [String] search terms
68
+ # @param start [String, nil] pagination token from a previous {SearchResult#next_page}
69
+ # @param filters [Hash] optional snake_case filter keys (same as mapping job filters)
70
+ # @return [SearchResult]
71
+ #
72
+ # @example
73
+ # result = client.search(query: "Apple", exch_code: "US")
74
+ # result.data #=> [#<struct OpenfigiRuby::FigiResult ...>, ...]
75
+ # result.next_page #=> "BBG..." or nil
76
+ def search(query:, start: nil, **filters)
77
+ body = { "query" => query }
78
+ body["start"] = start if start
79
+ body.merge!(serialize(filters))
80
+ response = post("/search", body)
81
+ parse_search_result(response)
82
+ end
83
+
84
+ # Filters FIGIs with alphabetical ordering and a total count (POST /v3/filter).
85
+ #
86
+ # @param query [String, nil] optional search terms
87
+ # @param start [String, nil] pagination token from a previous {FilterResult#next_page}
88
+ # @param filters [Hash] optional snake_case filter keys
89
+ # @return [FilterResult]
90
+ def filter(query: nil, start: nil, **filters)
91
+ body = {}
92
+ body["query"] = query if query
93
+ body["start"] = start if start
94
+ body.merge!(serialize(filters))
95
+ response = post("/filter", body)
96
+ parse_filter_result(response)
97
+ end
98
+
99
+ private
100
+
101
+ # OpenFIGI V3 API base URL.
102
+ # @api private
103
+ BASE_URL = "https://api.openfigi.com/v3"
104
+
105
+ def post(path, body)
106
+ uri = URI("#{BASE_URL}#{path}")
107
+ http = build_http(uri)
108
+ request = Net::HTTP::Post.new(uri)
109
+ apply_headers(request)
110
+ request.body = body.to_json
111
+ handle_response(http.request(request))
112
+ end
113
+
114
+ def get(path)
115
+ uri = URI("#{BASE_URL}#{path}")
116
+ http = build_http(uri)
117
+ request = Net::HTTP::Get.new(uri)
118
+ apply_headers(request)
119
+ handle_response(http.request(request))
120
+ end
121
+
122
+ def build_http(uri)
123
+ http = Net::HTTP.new(uri.host, uri.port)
124
+ http.use_ssl = uri.scheme == "https"
125
+ http.open_timeout = @open_timeout
126
+ http.read_timeout = @read_timeout
127
+ http
128
+ end
129
+
130
+ def apply_headers(request)
131
+ request["Content-Type"] = "application/json"
132
+ request["X-OPENFIGI-APIKEY"] = @api_key if @api_key
133
+ end
134
+
135
+ def handle_response(response)
136
+ body = response.body
137
+ code = response.code.to_i
138
+
139
+ case code
140
+ when 200
141
+ JSON.parse(body)
142
+ when 400
143
+ raise InvalidRequestError.new("Invalid request payload", status_code: code, body: body)
144
+ when 401
145
+ raise AuthenticationError.new("Invalid API key", status_code: code, body: body)
146
+ when 429
147
+ raise RateLimitError.new("Rate limit exceeded", status_code: code, body: body)
148
+ when 500, 503
149
+ raise ServerError.new("Server error (#{code})", status_code: code, body: body)
150
+ else
151
+ raise ApiError.new("Unexpected response (#{code})", status_code: code, body: body)
152
+ end
153
+ end
154
+
155
+ # Converts a hash with snake_case symbol keys to camelCase string keys,
156
+ # dropping any nil values.
157
+ def serialize(hash)
158
+ hash.each_with_object({}) do |(key, value), result|
159
+ next if value.nil?
160
+
161
+ result[camelize(key.to_s)] = value
162
+ end
163
+ end
164
+
165
+ # "id_type" => "idType", "security_type2" => "securityType2"
166
+ def camelize(snake_str)
167
+ parts = snake_str.split("_")
168
+ parts[0] + parts[1..].map(&:capitalize).join
169
+ end
170
+
171
+ def parse_mapping_result(item)
172
+ if item.key?("data")
173
+ MappingResult.new(data: item["data"].map { |r| build_figi_result(r) })
174
+ else
175
+ MappingResult.new(warning: item["warning"])
176
+ end
177
+ end
178
+
179
+ def parse_search_result(response)
180
+ SearchResult.new(
181
+ data: Array(response["data"]).map { |r| build_figi_result(r) },
182
+ next_page: response["next"],
183
+ error: response["error"]
184
+ )
185
+ end
186
+
187
+ def parse_filter_result(response)
188
+ FilterResult.new(
189
+ data: Array(response["data"]).map { |r| build_figi_result(r) },
190
+ next_page: response["next"],
191
+ total: response["total"],
192
+ error: response["error"]
193
+ )
194
+ end
195
+
196
+ def build_figi_result(hash)
197
+ FigiResult.new(
198
+ figi: hash["figi"],
199
+ security_type: hash["securityType"],
200
+ market_sector: hash["marketSector"],
201
+ ticker: hash["ticker"],
202
+ name: hash["name"],
203
+ exch_code: hash["exchCode"],
204
+ share_class_figi: hash["shareClassFIGI"],
205
+ composite_figi: hash["compositeFIGI"],
206
+ security_type2: hash["securityType2"],
207
+ security_description: hash["securityDescription"],
208
+ metadata: hash["metadata"]
209
+ )
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenfigiRuby
4
+ # Holds configuration for the gem. Set values via {OpenfigiRuby.configure}.
5
+ #
6
+ # @!attribute [rw] api_key
7
+ # @return [String, nil] OpenFIGI API key. Without a key, stricter rate limits apply.
8
+ # @!attribute [rw] open_timeout
9
+ # @return [Integer] seconds to wait when opening a connection (default: 10)
10
+ # @!attribute [rw] read_timeout
11
+ # @return [Integer] seconds to wait for a response (default: 30)
12
+ class Configuration
13
+ # OpenFIGI V3 API base URL.
14
+ # @api private
15
+ BASE_URL = "https://api.openfigi.com/v3"
16
+
17
+ attr_accessor :api_key, :open_timeout, :read_timeout
18
+
19
+ def initialize
20
+ @api_key = nil
21
+ @open_timeout = 10
22
+ @read_timeout = 30
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenfigiRuby
4
+ # Base error class for all OpenfigiRuby errors.
5
+ class Error < StandardError; end
6
+
7
+ # Raised when the API returns a non-successful HTTP status.
8
+ #
9
+ # @!attribute [r] status_code
10
+ # @return [Integer, nil] HTTP status code from the response
11
+ # @!attribute [r] body
12
+ # @return [String, nil] raw response body
13
+ class ApiError < Error
14
+ attr_reader :status_code, :body
15
+
16
+ def initialize(message = nil, status_code: nil, body: nil)
17
+ super(message)
18
+ @status_code = status_code
19
+ @body = body
20
+ end
21
+ end
22
+
23
+ # Raised on HTTP 401 — invalid or missing API key.
24
+ class AuthenticationError < ApiError; end
25
+
26
+ # Raised on HTTP 429 — rate limit exceeded.
27
+ class RateLimitError < ApiError; end
28
+
29
+ # Raised on HTTP 400 — malformed request payload.
30
+ class InvalidRequestError < ApiError; end
31
+
32
+ # Raised on HTTP 500/503 — transient server errors (retry with backoff).
33
+ class ServerError < ApiError; end
34
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenfigiRuby
4
+ # Represents a single financial instrument returned by the OpenFIGI API.
5
+ #
6
+ # @!attribute [r] figi
7
+ # @return [String] the Financial Instrument Global Identifier (FIGI)
8
+ # @!attribute [r] security_type
9
+ # @return [String, nil] primary security classification (e.g. "Common Stock", "ETP")
10
+ # @!attribute [r] market_sector
11
+ # @return [String, nil] market sector (e.g. "Equity", "Corp", "Govt")
12
+ # @!attribute [r] ticker
13
+ # @return [String, nil] exchange ticker symbol
14
+ # @!attribute [r] name
15
+ # @return [String, nil] instrument name
16
+ # @!attribute [r] exch_code
17
+ # @return [String, nil] exchange code (e.g. "US", "LN")
18
+ # @!attribute [r] share_class_figi
19
+ # @return [String, nil] share class-level FIGI
20
+ # @!attribute [r] composite_figi
21
+ # @return [String, nil] composite FIGI (aggregates listings across exchanges)
22
+ # @!attribute [r] security_type2
23
+ # @return [String, nil] secondary security classification
24
+ # @!attribute [r] security_description
25
+ # @return [String, nil] short description of the security
26
+ # @!attribute [r] metadata
27
+ # @return [String, nil] optional metadata string returned by the API
28
+ FigiResult = Struct.new(
29
+ :figi,
30
+ :security_type,
31
+ :market_sector,
32
+ :ticker,
33
+ :name,
34
+ :exch_code,
35
+ :share_class_figi,
36
+ :composite_figi,
37
+ :security_type2,
38
+ :security_description,
39
+ :metadata,
40
+ keyword_init: true
41
+ )
42
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenfigiRuby
4
+ # Result for a single job in a bulk mapping request.
5
+ #
6
+ # On success, {data} is an array of {FigiResult} objects and {warning} is nil.
7
+ # When no match is found, {data} is nil and {warning} holds the API message.
8
+ #
9
+ # @!attribute [r] data
10
+ # @return [Array<FigiResult>, nil] matched instruments, or nil when no match was found
11
+ # @!attribute [r] warning
12
+ # @return [String, nil] API warning message when no identifier was found
13
+ MappingResult = Struct.new(:data, :warning, keyword_init: true) do
14
+ # Returns true if the job matched at least one instrument.
15
+ # @return [Boolean]
16
+ def found?
17
+ !data.nil? && !data.empty?
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenfigiRuby
4
+ # Result from POST /v3/search.
5
+ #
6
+ # @!attribute [r] data
7
+ # @return [Array<FigiResult>] instruments matching the search query for this page
8
+ # @!attribute [r] next_page
9
+ # @return [String, nil] opaque pagination token; pass as +start:+ to {Client#search} to fetch
10
+ # the next page. nil when this is the last page.
11
+ # @!attribute [r] error
12
+ # @return [String, nil] error message when the query itself is invalid
13
+ SearchResult = Struct.new(:data, :next_page, :error, keyword_init: true)
14
+
15
+ # Result from POST /v3/filter.
16
+ #
17
+ # Like {SearchResult} but results are sorted alphabetically by FIGI and a total count is included.
18
+ #
19
+ # @!attribute [r] data
20
+ # @return [Array<FigiResult>] instruments matching the filter for this page
21
+ # @!attribute [r] next_page
22
+ # @return [String, nil] opaque pagination token; pass as +start:+ to {Client#filter} to fetch
23
+ # the next page. nil when this is the last page.
24
+ # @!attribute [r] total
25
+ # @return [Integer] total number of matching instruments across all pages
26
+ # @!attribute [r] error
27
+ # @return [String, nil] error message when the request is invalid
28
+ FilterResult = Struct.new(:data, :next_page, :total, :error, keyword_init: true)
29
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenfigiRuby
4
+ # Gem version string.
5
+ # @api private
6
+ VERSION = "0.1.0"
7
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "openfigi_ruby/version"
4
+ require_relative "openfigi_ruby/configuration"
5
+ require_relative "openfigi_ruby/error"
6
+ require_relative "openfigi_ruby/figi_result"
7
+ require_relative "openfigi_ruby/mapping_result"
8
+ require_relative "openfigi_ruby/search_result"
9
+ require_relative "openfigi_ruby/client"
10
+
11
+ # Ruby client for the OpenFIGI V3 API.
12
+ #
13
+ # Provides identifier mapping, keyword search, and filtering for Financial
14
+ # Instrument Global Identifiers (FIGIs).
15
+ #
16
+ # @example Configure globally and create a client
17
+ # OpenfigiRuby.configure do |config|
18
+ # config.api_key = ENV["OPENFIGI_API_KEY"]
19
+ # end
20
+ #
21
+ # client = OpenfigiRuby::Client.new
22
+ # results = client.mapping([{ id_type: "ID_ISIN", id_value: "US0378331005" }])
23
+ module OpenfigiRuby
24
+ class << self
25
+ # Returns the global {Configuration} instance.
26
+ # @return [Configuration]
27
+ def configuration
28
+ @configuration ||= Configuration.new
29
+ end
30
+
31
+ # Configures the gem globally.
32
+ #
33
+ # @example
34
+ # OpenfigiRuby.configure do |config|
35
+ # config.api_key = ENV["OPENFIGI_API_KEY"]
36
+ # end
37
+ # @yieldparam config [Configuration]
38
+ # @return [void]
39
+ def configure
40
+ yield configuration
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,4 @@
1
+ module OpenfigiRuby
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: openfigi_ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Phong Si
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-05-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: webmock
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: yard
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.9'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.9'
55
+ - !ruby/object:Gem::Dependency
56
+ name: webrick
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ description: Maps financial identifiers (ISIN, CUSIP, ticker, etc.) to FIGIs via the
70
+ OpenFIGI V3 API. Supports bulk mapping, keyword search, and filtering.
71
+ email: []
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".yardopts"
77
+ - CLAUDE.md
78
+ - LICENSE.txt
79
+ - README.md
80
+ - Rakefile
81
+ - lib/openfigi_ruby.rb
82
+ - lib/openfigi_ruby/client.rb
83
+ - lib/openfigi_ruby/configuration.rb
84
+ - lib/openfigi_ruby/error.rb
85
+ - lib/openfigi_ruby/figi_result.rb
86
+ - lib/openfigi_ruby/mapping_result.rb
87
+ - lib/openfigi_ruby/search_result.rb
88
+ - lib/openfigi_ruby/version.rb
89
+ - sig/openfigi_ruby.rbs
90
+ homepage: https://github.com/phongsi/openfigi_ruby
91
+ licenses:
92
+ - MIT
93
+ metadata:
94
+ homepage_uri: https://github.com/phongsi/openfigi_ruby
95
+ source_code_uri: https://github.com/phongsi/openfigi_ruby
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: 3.1.0
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubygems_version: 3.4.19
112
+ signing_key:
113
+ specification_version: 4
114
+ summary: Ruby client for the OpenFIGI V3 API
115
+ test_files: []