diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml new file mode 100644 index 0000000..5338e9c --- /dev/null +++ b/.github/workflows/typecheck.yml @@ -0,0 +1,20 @@ +name: typecheck.yml +on: + pull_request: +jobs: + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.4" + bundler-cache: true + + - name: Install RBS collections + run: rbs collection install + + - name: Run type checker + run: bundle exec steep check --severity-level=error diff --git a/.gitignore b/.gitignore index bd00448..781633b 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,6 @@ yarn-debug.log* /lib/bundler/man/ # End of https://www.toptal.com/developers/gitignore/api/rails,ruby + +.gem_rbs_collection +.idea \ No newline at end of file diff --git a/Gemfile b/Gemfile index 4541a26..bd161c6 100644 --- a/Gemfile +++ b/Gemfile @@ -19,6 +19,7 @@ group :development, :test do gem 'rake' gem 'rubocop' gem 'simplecov', require: false + gem 'steep', require: false gem 'vcr' gem 'webmock' end diff --git a/Gemfile.lock b/Gemfile.lock index 78bbca2..dbaa6c0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -17,9 +17,11 @@ GEM bigdecimal (3.3.1) byebug (12.0.0) cgi (0.5.2) + concurrent-ruby (1.3.8) crack (1.0.1) bigdecimal rexml + csv (3.3.6) docile (1.4.1) drb (2.2.3) excon (1.3.1) @@ -34,10 +36,17 @@ GEM net-http (~> 0.5) faraday-retry (2.4.0) faraday (~> 2.0) + ffi (1.17.4) + ffi (1.17.4-x86_64-darwin) + fileutils (1.8.0) hashdiff (1.2.1) json (2.20.0) language_server-protocol (3.17.0.5) lint_roller (1.1.0) + listen (3.10.0) + logger + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) logger (1.7.0) minitest (6.0.6) drb (~> 2.0) @@ -57,6 +66,13 @@ GEM racc (1.8.1) rainbow (3.1.1) rake (13.3.1) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) + ffi (~> 1.0) + rbs (4.1.2) + logger + prism (>= 1.6.0) + tsort regexp_parser (2.11.3) rexml (3.4.4) rubocop (1.81.7) @@ -75,12 +91,33 @@ GEM prism (~> 1.4) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) + securerandom (0.4.1) simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) simplecov-html (0.13.2) simplecov_json_formatter (0.1.4) + steep (2.0.0) + concurrent-ruby (>= 1.1.10) + csv (>= 3.0.9) + fileutils (>= 1.1.0) + json (>= 2.1.0) + language_server-protocol (>= 3.17.0.4, < 4.0) + listen (~> 3.0) + logger (>= 1.3.0) + parser (>= 3.2) + prism (>= 0.25.0) + rainbow (>= 2.2.2, < 4.0) + rbs (~> 4.0) + securerandom (>= 0.1) + strscan (>= 1.0.0) + terminal-table (>= 2, < 5) + uri (>= 0.12.0) + strscan (3.1.8) + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) + tsort (0.2.0) unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.2.0) @@ -109,6 +146,7 @@ DEPENDENCIES rake rubocop simplecov + steep vcr webmock diff --git a/Steepfile b/Steepfile new file mode 100644 index 0000000..5e37350 --- /dev/null +++ b/Steepfile @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +target :lib do + check 'lib' + signature 'sig' +end diff --git a/data_services_api.gemspec b/data_services_api.gemspec index 1a5cca6..b248099 100644 --- a/data_services_api.gemspec +++ b/data_services_api.gemspec @@ -13,7 +13,8 @@ Gem::Specification.new do |spec| spec.license = 'MIT' spec.required_ruby_version = '>= 3.4' - spec.files = Dir.glob('lib/**/*', File::FNM_DOTMATCH) + ['LICENSE.txt', 'README.md'] + spec.files = Dir.glob('{lib/**/*,sig/**/*.rbs}', + File::FNM_DOTMATCH) + ['LICENSE.txt', 'README.md'] spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.extra_rdoc_files = Dir['README.md', 'CHANGELOG.md', 'LICENSE.txt'] spec.require_paths = ['lib'] diff --git a/lib/data_services_api/aspect.rb b/lib/data_services_api/aspect.rb index b603212..28a08b9 100644 --- a/lib/data_services_api/aspect.rb +++ b/lib/data_services_api/aspect.rb @@ -13,7 +13,7 @@ def initialize(json, service) @service = service end - def id + def id # steep:ignore MethodBodyTypeMismatch @json['@id'] end diff --git a/lib/data_services_api/dataset.rb b/lib/data_services_api/dataset.rb index 6f595cc..7038ba7 100644 --- a/lib/data_services_api/dataset.rb +++ b/lib/data_services_api/dataset.rb @@ -10,12 +10,12 @@ def initialize(json, service) @service = service end - def id + def id # steep:ignore MethodBodyTypeMismatch @json['@id'] end def method_missing(attribute) - respond_to_missing?(attribute) ? @json[attribute.to_s] : super + respond_to_missing?(attribute, nil) ? @json[attribute.to_s] : super end def respond_to_missing?(attribute, _args = nil) @@ -26,7 +26,7 @@ def data_api @json['data-api'] end - def dataset + def dataset # steep:ignore MethodBodyTypeMismatch @json['dataset'] end diff --git a/lib/data_services_api/service.rb b/lib/data_services_api/service.rb index d658760..4f7c8a2 100644 --- a/lib/data_services_api/service.rb +++ b/lib/data_services_api/service.rb @@ -130,7 +130,7 @@ def perform_request(http_url, method, query_params = nil) # rubocop:disable Metr def create_http_connection(http_url) # rubocop:disable Metrics/MethodLength Faraday.new(url: http_url) do |config| - config.options[:timeout] = @connection_timeout + config.options.timeout = @connection_timeout config.use Faraday::Request::UrlEncoded config.use Faraday::FollowRedirects::Middleware diff --git a/lib/data_services_api/value.rb b/lib/data_services_api/value.rb index 479e010..31e0e4a 100644 --- a/lib/data_services_api/value.rb +++ b/lib/data_services_api/value.rb @@ -12,19 +12,19 @@ def initialize(base = {}, adds = {}) end def value - self[:@value] + self['@value'] end - def type - self[:@type] + def type # steep:ignore MethodBodyTypeMismatch + self['@type'] end - def uri - self[:@id] + def uri # steep:ignore MethodBodyTypeMismatch + self['@id'] end def with_uri(uri) - Value.new(self, '@id': uri) + Value.new(self, { '@id' => uri }) end def self.uri(uri) @@ -32,7 +32,7 @@ def self.uri(uri) end def with_typed_value(value, type) - Value.new(self, '@value': value, '@type': type) + Value.new(self, { '@value' => value, '@type' => type }) end def with_year_month(year, month) diff --git a/rbs_collection.lock.yaml b/rbs_collection.lock.yaml new file mode 100644 index 0000000..5b810b3 --- /dev/null +++ b/rbs_collection.lock.yaml @@ -0,0 +1,200 @@ +--- +path: ".gem_rbs_collection" +gems: +- name: addressable + version: '2.8' + source: + type: git + name: ruby/gem_rbs_collection + revision: 82762423fc5694b82888ea89a972847cf19838c2 + remote: https://github.com/ruby/gem_rbs_collection.git + repo_dir: gems +- name: ast + version: '2.4' + source: + type: git + name: ruby/gem_rbs_collection + revision: 82762423fc5694b82888ea89a972847cf19838c2 + remote: https://github.com/ruby/gem_rbs_collection.git + repo_dir: gems +- name: base64 + version: 0.3.0 + source: + type: rubygems +- name: bigdecimal + version: '3.1' + source: + type: git + name: ruby/gem_rbs_collection + revision: 82762423fc5694b82888ea89a972847cf19838c2 + remote: https://github.com/ruby/gem_rbs_collection.git + repo_dir: gems +- name: cgi + version: '0.5' + source: + type: git + name: ruby/gem_rbs_collection + revision: 82762423fc5694b82888ea89a972847cf19838c2 + remote: https://github.com/ruby/gem_rbs_collection.git + repo_dir: gems +- name: delegate + version: '0' + source: + type: stdlib +- name: faraday + version: '2.7' + source: + type: git + name: ruby/gem_rbs_collection + revision: 82762423fc5694b82888ea89a972847cf19838c2 + remote: https://github.com/ruby/gem_rbs_collection.git + repo_dir: gems +- name: fileutils + version: '0' + source: + type: stdlib +- name: forwardable + version: '0' + source: + type: stdlib +- name: hashdiff + version: '1.1' + source: + type: git + name: ruby/gem_rbs_collection + revision: 82762423fc5694b82888ea89a972847cf19838c2 + remote: https://github.com/ruby/gem_rbs_collection.git + repo_dir: gems +- name: json + version: '0' + source: + type: stdlib +- name: lint_roller + version: '1.1' + source: + type: git + name: ruby/gem_rbs_collection + revision: 82762423fc5694b82888ea89a972847cf19838c2 + remote: https://github.com/ruby/gem_rbs_collection.git + repo_dir: gems +- name: logger + version: '1.7' + source: + type: git + name: ruby/gem_rbs_collection + revision: 82762423fc5694b82888ea89a972847cf19838c2 + remote: https://github.com/ruby/gem_rbs_collection.git + repo_dir: gems +- name: minitest + version: '5.25' + source: + type: git + name: ruby/gem_rbs_collection + revision: 82762423fc5694b82888ea89a972847cf19838c2 + remote: https://github.com/ruby/gem_rbs_collection.git + repo_dir: gems +- name: monitor + version: '0' + source: + type: stdlib +- name: mutex_m + version: 0.3.0 + source: + type: rubygems +- name: net-http + version: '0' + source: + type: stdlib +- name: net-protocol + version: '0' + source: + type: stdlib +- name: parallel + version: '1.20' + source: + type: git + name: ruby/gem_rbs_collection + revision: 82762423fc5694b82888ea89a972847cf19838c2 + remote: https://github.com/ruby/gem_rbs_collection.git + repo_dir: gems +- name: parser + version: '3.2' + source: + type: git + name: ruby/gem_rbs_collection + revision: 82762423fc5694b82888ea89a972847cf19838c2 + remote: https://github.com/ruby/gem_rbs_collection.git + repo_dir: gems +- name: prism + version: 1.9.0 + source: + type: rubygems +- name: rainbow + version: '3.0' + source: + type: git + name: ruby/gem_rbs_collection + revision: 82762423fc5694b82888ea89a972847cf19838c2 + remote: https://github.com/ruby/gem_rbs_collection.git + repo_dir: gems +- name: rake + version: '13.0' + source: + type: git + name: ruby/gem_rbs_collection + revision: 82762423fc5694b82888ea89a972847cf19838c2 + remote: https://github.com/ruby/gem_rbs_collection.git + repo_dir: gems +- name: regexp_parser + version: '2.8' + source: + type: git + name: ruby/gem_rbs_collection + revision: 82762423fc5694b82888ea89a972847cf19838c2 + remote: https://github.com/ruby/gem_rbs_collection.git + repo_dir: gems +- name: rubocop + version: '1.57' + source: + type: git + name: ruby/gem_rbs_collection + revision: 82762423fc5694b82888ea89a972847cf19838c2 + remote: https://github.com/ruby/gem_rbs_collection.git + repo_dir: gems +- name: rubocop-ast + version: '1.46' + source: + type: git + name: ruby/gem_rbs_collection + revision: 82762423fc5694b82888ea89a972847cf19838c2 + remote: https://github.com/ruby/gem_rbs_collection.git + repo_dir: gems +- name: stringio + version: '0' + source: + type: stdlib +- name: tempfile + version: '0' + source: + type: stdlib +- name: timeout + version: '0' + source: + type: stdlib +- name: tmpdir + version: '0' + source: + type: stdlib +- name: uri + version: '0' + source: + type: stdlib +- name: webmock + version: '3.19' + source: + type: git + name: ruby/gem_rbs_collection + revision: 82762423fc5694b82888ea89a972847cf19838c2 + remote: https://github.com/ruby/gem_rbs_collection.git + repo_dir: gems +gemfile_lock_path: Gemfile.lock diff --git a/rbs_collection.yaml b/rbs_collection.yaml new file mode 100644 index 0000000..66e30ec --- /dev/null +++ b/rbs_collection.yaml @@ -0,0 +1,19 @@ +# Download sources +sources: + - type: git + name: ruby/gem_rbs_collection + remote: https://github.com/ruby/gem_rbs_collection.git + revision: main + repo_dir: gems + +# You can specify local directories as sources also. +# - type: local +# path: path/to/your/local/repository + +# A directory to install the downloaded RBSs +path: .gem_rbs_collection + +# gems: +# # If you want to avoid installing rbs files for gems, you can specify them here. +# - name: GEM_NAME +# ignore: true diff --git a/sig/data_services_api/aspect.rbs b/sig/data_services_api/aspect.rbs new file mode 100644 index 0000000..5dd804c --- /dev/null +++ b/sig/data_services_api/aspect.rbs @@ -0,0 +1,35 @@ +module DataServicesApi + # Encapsulates a DSAPI Aspect + # This corresponds to one of the measures of the underlying data cube, which + # becomes an _aspect_ of the object description that the DSApi query encapsulates. + # See https://github.com/epimorphics/data-API/wiki/Data-Query-Syntax + class Aspect + @json: json_hash + + @service: untyped + + attr_reader service: untyped + + def initialize: (json_hash json, untyped service) -> void + + def id: () -> uri + + alias uri id + + def method_missing: (untyped name) -> untyped + + def respond_to_missing?: (untyped name, ?untyped? _args) -> bool + + def optional?: () -> bool + + def multi_valued?: () -> bool + + def range_type: () -> uri + + def isOptional: () -> bool + + def isMultiValued: () -> bool + + def rangeType: () -> uri + end +end diff --git a/sig/data_services_api/dataset.rbs b/sig/data_services_api/dataset.rbs new file mode 100644 index 0000000..7845c89 --- /dev/null +++ b/sig/data_services_api/dataset.rbs @@ -0,0 +1,38 @@ +module DataServicesApi + # Encapsulates a single dataset from the data services API + class Dataset + @json: json_hash + + @service: untyped + + @structure: untyped + + attr_reader service: untyped + + def initialize: (json_hash json, untyped service) -> void + + def id: () -> uri + + def method_missing: (untyped attribute) -> untyped + + def respond_to_missing?: (untyped attribute, ?untyped? _args) -> bool + + def data_api: () -> untyped + + def dataset: () -> Dataset + + def structure_api: () -> untyped + + def describe_api: () -> untyped + + def explain_api: () -> untyped + + def structure: () -> untyped + + def query: (untyped query) -> untyped + + def describe: (untyped uri) -> untyped + + def explain: (untyped query) -> untyped + end +end diff --git a/sig/data_services_api/dsapi_response_converter.rbs b/sig/data_services_api/dsapi_response_converter.rbs new file mode 100644 index 0000000..f1ea92b --- /dev/null +++ b/sig/data_services_api/dsapi_response_converter.rbs @@ -0,0 +1,28 @@ +# :nodoc: +module DataServicesApi + # Adapter-pattern to present a response from SapiNT in the same JSON structure + # that the old DsAPI was using + class DSAPIResponseConverter + @sapint_response: untyped + + @dataset_name: untyped + + @json_mode_compact: untyped + + def initialize: (untyped sapint_response, untyped dataset_name, ?bool json_mode_compact) -> void + + # Converts SAPINT returned JSON format to DSAPI returned JSON format + def to_dsapi_response: () -> untyped + + private + + def to_dsapi_item: (untyped sapint_item) -> untyped + + # Return different response formats based on the set JSON mode + def to_dsapi_json: (untyped sapint_key, untyped sapint_value) -> untyped + + def json_mode_compact: (untyped sapint_key, untyped sapint_value) -> (::Hash[untyped, untyped] | ::Hash[::String, untyped] | untyped) + + def json_mode_complete: (untyped sapint_key, untyped sapint_value) -> (::Hash[untyped, untyped] | ::Hash[::String, { "@value" => untyped }] | ::Hash[::String, ::Array[{ "@value" => untyped }]] | ::Hash[::String, ::Array[untyped]] | ::Hash[::String, untyped]) + end +end diff --git a/sig/data_services_api/query_generator.rbs b/sig/data_services_api/query_generator.rbs new file mode 100644 index 0000000..6fdb1a9 --- /dev/null +++ b/sig/data_services_api/query_generator.rbs @@ -0,0 +1,60 @@ +module DataServicesApi + # Generate Data Service API queries, and serialize as JSON + class QueryGenerator + @terms: untyped + + OPERATIONS: ::Array["@eq" | "@ge" | "@gt" | "@le" | "@lt"] + + attr_reader terms: untyped + + def initialize: (?untyped? initial_terms) -> void + + def to_json: (*untyped _args) -> untyped + + def compact_json: () -> untyped + + def eq: (untyped attribute, untyped value) -> untyped + + def eq_any_uri: (untyped attribute, untyped uris, ?::Hash[untyped, untyped] options) -> untyped + + def eq_any_value: (untyped attribute, untyped values, ?::Hash[untyped, untyped] options) -> untyped + + def ge: (untyped attribute, untyped value) -> untyped + + def gt: (untyped attribute, untyped value) -> untyped + + def le: (untyped attribute, untyped value) -> untyped + + def lt: (untyped attribute, untyped value) -> untyped + + def op: (untyped oper, untyped attribute, untyped value) -> untyped + + def sort: (untyped up_or_down, untyped attribute) -> untyped + + def search: (untyped pattern) -> untyped + + def search_property: (untyped property, untyped pattern, ?::Hash[untyped, untyped] options) -> untyped + + def search_aspect: (untyped aspect, untyped pattern) -> untyped + + def search_aspect_property: (untyped aspect, untyped property, untyped pattern, ?::Hash[untyped, untyped] options) -> untyped + + def matches: (untyped aspect, untyped pattern, ?::Hash[untyped, untyped] options) -> untyped + + def limit: (untyped lim) -> untyped + + def offset: (untyped num) -> untyped + + def count_only: () -> untyped + + private + + def relational: (untyped rel, untyped attribute, untyped value) -> untyped + + def eq_any: (untyped attribute, untyped values, untyped value_type, untyped options) -> untyped + + def as_typed_value: (untyped value) -> untyped + + def merge_terms: (untyped term) -> untyped + end +end diff --git a/sig/data_services_api/sapint_converter.rbs b/sig/data_services_api/sapint_converter.rbs new file mode 100644 index 0000000..2c78814 --- /dev/null +++ b/sig/data_services_api/sapint_converter.rbs @@ -0,0 +1,42 @@ +module DataServicesApi + # Transformer to convert queries using the DsAPI query DSL + # into an equivalent SapiNT query URL string + class SapiNTConverter + @dsapi_query: untyped + + def initialize: (untyped dsapi_query) -> void + + # Converts a DSAPI query to SAPINT query + def to_sapint_query: () -> untyped + + private + + def sapint_query: (untyped key, untyped value) -> untyped + + def sort: (untyped values) -> untyped + + def count: (untyped value) -> ({ "_count" => "@id" } | nil) + + def limit: (untyped value) -> { "_limit" => untyped } + + def offset: (untyped value) -> { "_offset" => untyped } + + def and_list: (untyped list) -> untyped + + def and_item: (untyped json_item) -> untyped + + def relation: (untyped relation, untyped attribute, untyped json) -> untyped + + def eq: (untyped attribute, untyped value) -> ::Hash[untyped, untyped] + + def oneof: (untyped attribute, untyped values) -> untyped + + def comparison: (untyped prefix, untyped attribute, untyped value) -> ::Hash[::String, untyped] + + def search: (untyped attribute, untyped value) -> ::Hash["searchPath" | ::String, untyped] + + def sanitize_search: (untyped search_text) -> untyped + + def remove_prefix: (untyped value) -> untyped + end +end diff --git a/sig/data_services_api/service.rbs b/sig/data_services_api/service.rbs new file mode 100644 index 0000000..5f9e2f2 --- /dev/null +++ b/sig/data_services_api/service.rbs @@ -0,0 +1,117 @@ +module DataServicesApi + # Denotes the encapsulated DataServicesAPI service + class Service + @instrumenter: untyped + + @faraday_logger: untyped + + @faraday_logger_options: untyped + + @url: uri + + @connection_timeout: int + + @connection_failed_retry_options: untyped + + @timeout_retry_options: untyped + + attr_reader instrumenter: untyped + + attr_reader url: untyped + + DEFAULT_CONNECTION_FAILED_RETRY_OPTIONS: { max: 4, interval: ::Float, interval_randomness: ::Float, backoff_factor: 2, exceptions: ::Array[untyped] } + + DEFAULT_TIMEOUT_RETRY_OPTIONS: { max: 2, interval: ::Float, interval_randomness: ::Float, backoff_factor: 2, exceptions: ::Array[untyped] } + + DEFAULT_FARADAY_LOGGER_OPTIONS: { headers: false, bodies: false, errors: false, log_level: :debug } + + DEFAULT_CONNECTION_TIMEOUT_SECONDS: 600 + + def initialize: (?::Hash[untyped, untyped] config) -> void + + def datasets: () -> Array[Dataset] + + def dataset: (untyped name) -> Dataset + + def api_get_json: (untyped api, untyped params, ?::Hash[untyped, untyped] options) -> json_value + + def api_post_json: (untyped api, untyped json) -> json_value + + private + + # Get parsed JSON from the given URL + def get_json: (untyped http_url, untyped params, untyped options) -> json_value + + def get_from_api: (untyped http_url, untyped accept_headers, untyped params, untyped options) -> untyped + + def post_json: (untyped http_url, untyped json) -> untyped + + def post_to_api: (untyped http_url, untyped json) -> untyped + + # Perform an HTTP request against http_url, timing and instrumenting it consistently + # regardless of whether it succeeds, times out, fails to connect, or the remote API + # returns an error status or unparseable body. query_params, when given, is only used + # to report the query string on connection/service failures (a successful response + # reports its own resolved query string from the Faraday response itself) + def perform_request: (untyped http_url, untyped method, ?untyped? query_params) { (untyped) -> untyped } -> untyped + + def create_http_connection: (untyped http_url) -> untyped + + # Add a retry_block to the given faraday-retry options that fires a + # retry.data_services_api notification before each retry attempt, preserving any + # retry_block the caller already configured + def with_retry_instrumentation: (untyped options) -> untyped + + # Fires 'retry.data_services_api' immediately before a retry attempt, with + # path (String), method (String, upcased), retry_count (Integer), + # exception, and will_retry_in (Float seconds until the retry fires - note + # this is seconds, not the milliseconds :duration uses on the other events + # below). exception is normally Faraday::TimeoutError or ConnectionFailed; + # it would be the synthetic Faraday::RetriableResponse if a status-code-based + # retry_statuses: were ever configured, which this gem doesn't set today. + def instrument_retry: (untyped env, untyped retry_count, untyped exception, untyped will_retry_in) -> untyped + + # Fires 'request.data_services_api' immediately before a request is sent, with + # path (String), method (String), and query_string (String or nil - nil for + # POST requests, which never pass query_params, and for GET requests with + # no params). + def instrument_request: (untyped http_url, untyped method, untyped query_params) -> untyped + + def as_http_api: (untyped api) -> untyped + + # Fires 'response.data_services_api' for every response that doesn't raise, + # with response (the raw Faraday::Response - path/query_string/method are + # all derivable from response.env.url/.method rather than duplicated as + # separate payload keys) and duration (Integer milliseconds, floor-divided + # by #elapsed_ms - sub-millisecond requests report 0, not a fractional value). + def instrument_response: (untyped response, untyped start_time) -> untyped + + # Fires 'connection_failure.data_services_api' on a network-level failure + # (after retries are exhausted): the request never got a response at all. + # Payload: exception (Faraday::TimeoutError or ConnectionFailed), path + # (String, no scheme/host/query), query_string (String or nil - nil for + # POST requests, which never pass query_params, and for GET requests with + # no params), duration (Integer milliseconds, see #instrument_response), + # and status (always the literal 503 - a fixed value, not derived from any + # actual response, since none was received). + def instrument_connection_failure: (untyped http_url, untyped query_params, untyped exception, untyped start_time) -> untyped + + # Fires 'service_exception.data_services_api' when the remote API responded + # but with an error status or an unparseable body. exception is always a + # ServiceException here: perform_request wraps every Faraday::Error (bad + # status, unparseable body, etc) into one before raising, so subscribers + # never see a raw Faraday::ResourceNotFound/ClientError/ServerError/ParsingError. + # Payload also has path and query_string (same shape/nilability as + # #instrument_connection_failure), duration (Integer milliseconds), and + # status (Integer or nil - nil if Faraday never associated a response with + # the error; see Faraday::Error#response_status, which can happen for some + # Faraday::ParsingError cases). + def instrument_service_exception: (untyped http_url, untyped query_params, untyped exception, untyped start_time) -> untyped + + # Return true if we're currently running in a Rails environment + def in_rails?: () -> untyped + + # The elapsed time in milliseconds since the given CLOCK_MONOTONIC microsecond timestamp + def elapsed_ms: (untyped start_time) -> untyped + end +end diff --git a/sig/data_services_api/service_exception.rbs b/sig/data_services_api/service_exception.rbs new file mode 100644 index 0000000..453d45b --- /dev/null +++ b/sig/data_services_api/service_exception.rbs @@ -0,0 +1,19 @@ +module DataServicesApi + # A sub-type of RuntimeError for problems that arise when interacting with the + # DS API + class ServiceException < RuntimeError + @status: int? + + @source: untyped + + @service_message: string? + + attr_reader status: int? + + attr_reader source: untyped + + attr_reader service_message: string? + + def initialize: (string? msg, int? status, ?untyped? source, ?untyped? service_message) -> void + end +end diff --git a/sig/data_services_api/value.rbs b/sig/data_services_api/value.rbs new file mode 100644 index 0000000..0d6fbae --- /dev/null +++ b/sig/data_services_api/value.rbs @@ -0,0 +1,22 @@ +module DataServicesApi + # Encapsulates a single value coming back from the API + class Value < Hash[string, json_value] + def initialize: (?::Hash[string, json_value] base, ?::Hash[string, json_value] adds) -> void + + def value: () -> json_value + + def type: () -> uri + + def uri: () -> uri + + def with_uri: (uri uri) -> Value + + def self.uri: (uri uri) -> Value + + def with_typed_value: (untyped value, untyped type) -> Value + + def with_year_month: (untyped year, untyped month) -> Value + + def self.year_month: (untyped year, untyped month) -> Value + end +end diff --git a/sig/data_services_api/version.rbs b/sig/data_services_api/version.rbs new file mode 100644 index 0000000..8f16ca6 --- /dev/null +++ b/sig/data_services_api/version.rbs @@ -0,0 +1,8 @@ +# :nodoc: +module DataServicesApi + MAJOR: int + MINOR: int + PATCH: int + SUFFIX: string? + VERSION: ::String +end diff --git a/sig/types.rbs b/sig/types.rbs new file mode 100644 index 0000000..254264f --- /dev/null +++ b/sig/types.rbs @@ -0,0 +1,4 @@ +type uri = string +type json_value = + String | Integer | Float | TrueClass | FalseClass | NilClass | Array[json_value] | Hash[string, json_value] | uri +type json_hash = Hash[string, json_value] \ No newline at end of file diff --git a/test/data_services_api/value_test.rb b/test/data_services_api/value_test.rb index 84d231a..d7600ca 100644 --- a/test/data_services_api/value_test.rb +++ b/test/data_services_api/value_test.rb @@ -18,24 +18,24 @@ it 'should specify a URI' do v1 = v.with_uri('http://foo/bar') _(v1.size).must_equal 1 - _(v1[:@id]).must_equal 'http://foo/bar' + _(v1['@id']).must_equal 'http://foo/bar' _(v1.uri).must_equal 'http://foo/bar' end it 'should have a factory shortcut for creating a URI value' do v = DataServicesApi::Value.uri('http://fubar.com') _(v.size).must_equal 1 - _(v[:@id]).must_equal 'http://fubar.com' + _(v['@id']).must_equal 'http://fubar.com' end it 'should specify type and value' do v1 = v.with_typed_value('foo', 'http://fakexsd.org/bar') _(v1.size).must_equal 2 - _(v1[:@value]).must_equal 'foo' + _(v1['@value']).must_equal 'foo' _(v1.value).must_equal 'foo' - _(v1[:@type]).must_equal 'http://fakexsd.org/bar' + _(v1['@type']).must_equal 'http://fakexsd.org/bar' _(v1.type).must_equal 'http://fakexsd.org/bar' end