Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ jobs:
os:
- ubuntu-latest
ruby:
- "3.2"
- "3.3"
- "3.4"
- "4.0"
Expand Down
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ inherit_mode:

plugins: rubocop-performance
AllCops:
TargetRubyVersion: 3.2.0
TargetRubyVersion: 3.3.0
NewCops: enable
SuggestExtensions: false
Exclude:
Expand Down
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@

## Unreleased

## 3.3.1

- Optimized caching towards reducing retained memory after calling `OpenapiFirst.load` without using a global cache. (Removed `OpenapiFirst.clear_cache!`.)
- Require ruby >= 3.3.0

## 3.3.0

- OpenapiFirst will now cache the contents of files that have been loaded. If you need to reload your OpenAPI definition for tests or server hot reloading, you can call `OpenapiFirst.clear_cache!`.
- ~OpenapiFirst will now cache the contents of files that have been loaded. If you need to reload your OpenAPI definition for tests or server hot reloading, you can call `OpenapiFirst.clear_cache!`.~
- Optimized `OpenapiFirst::Router#match` for faster path matching and reduced memory allocation.

## 3.2.1
Expand Down
10 changes: 5 additions & 5 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
openapi_first (3.3.0)
openapi_first (3.3.1)
drb (~> 2.0)
hana (~> 1.3)
json_schemer (>= 2.1, < 3.0)
Expand Down Expand Up @@ -151,7 +151,7 @@ GEM
racc (~> 1.4)
openapi_parameters (0.11.0)
rack (>= 2.2)
parallel (1.28.0)
parallel (2.0.1)
parser (3.3.11.1)
ast (~> 2.4.1)
racc
Expand Down Expand Up @@ -227,11 +227,11 @@ GEM
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.7)
rubocop (1.86.0)
rubocop (1.86.1)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10)
parallel (>= 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
Expand Down Expand Up @@ -302,4 +302,4 @@ DEPENDENCIES
sinatra

BUNDLED WITH
4.0.6
4.0.10
4 changes: 2 additions & 2 deletions benchmarks/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: ..
specs:
openapi_first (3.3.0)
openapi_first (3.3.1)
drb (~> 2.0)
hana (~> 1.3)
json_schemer (>= 2.1, < 3.0)
Expand Down Expand Up @@ -42,7 +42,7 @@ GEM
optparse
uri
webrick
puma (7.2.0)
puma (8.0.0)
nio4r (~> 2.0)
rack (3.2.6)
rack-protection (4.2.1)
Expand Down
10 changes: 0 additions & 10 deletions benchmarks/apps/large.ru

This file was deleted.

6 changes: 5 additions & 1 deletion benchmarks/large.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@
require 'openapi_first'

Benchmark.memory do |x|
x.report { OpenapiFirst.load('../spec/data/large.yaml') }
x.report do
oad = OpenapiFirst.load('../spec/data/large.yaml')
request = Rack::Request.new(Rack::MockRequest.env_for('/workspaces'))
oad.validate_request(request)
end
end
5 changes: 0 additions & 5 deletions lib/openapi_first.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,6 @@ module OpenapiFirst

FAILURE = :openapi_first_validation_failure

# Clears cached files
def self.clear_cache!
FileLoader.clear_cache!
end

# @return [Configuration]
def self.configuration
@configuration ||= Configuration.new
Expand Down
8 changes: 5 additions & 3 deletions lib/openapi_first/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,20 @@ def initialize(contents, filepath:, config:)
meta_schema = detect_meta_schema(contents, filepath)
@schemer_configuration = build_schemer_config(filepath:, meta_schema:)
@config = config
@contents = RefResolver.for(contents, filepath:)
@file_loader = FileLoader.new
ref_resolver = RefResolver.new(file_loader:)
@contents = ref_resolver.for(contents, filepath:)
end

attr_reader :config
private attr_reader :schemer_configuration
private attr_reader :schemer_configuration, :file_loader

def build_schemer_config(filepath:, meta_schema:)
result = JSONSchemer.configuration.clone
dir = (filepath && File.absolute_path(File.dirname(filepath))) || Dir.pwd
result.base_uri = URI::File.build({ path: "#{dir}/" })
result.ref_resolver = JSONSchemer::CachedResolver.new do |uri|
FileLoader.load(uri.path)
file_loader.load(uri.path)
end
result.meta_schema = meta_schema
result.insert_property_defaults = true
Expand Down
16 changes: 8 additions & 8 deletions lib/openapi_first/file_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@

module OpenapiFirst
# @!visibility private
module FileLoader
@cache = {}
@mutex = Mutex.new
class FileLoader
def self.load(file_path)
new.load(file_path)
end

module_function
def initialize
@cache = {}
@mutex = Mutex.new
end

def load(file_path)
@cache[file_path] || @mutex.synchronize do
Expand All @@ -29,9 +33,5 @@ def load(file_path)
end
end
end

def clear_cache!
@mutex.synchronize { @cache.clear }
end
end
end
45 changes: 29 additions & 16 deletions lib/openapi_first/ref_resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,42 @@
module OpenapiFirst
# This is here to give traverse an OAD while keeping $refs intact
# @visibility private
module RefResolver
def self.load(filepath)
contents = OpenapiFirst::FileLoader.load(filepath)
class RefResolver
def initialize(file_loader:)
@file_loader = file_loader
end

private attr_reader :file_loader

def load(filepath)
contents = file_loader.load(filepath)
self.for(contents, filepath:)
end

def self.for(value, filepath: nil, context: value)
def for(value, filepath: nil, context: value)
case value
when ::Hash
resolver = Hash.new(value, context:, filepath:)
resolver = Hash.new(value, context:, filepath:, ref_resolver: self)
if value.key?('$ref')
probe = resolver.resolve_ref(value['$ref'])
return probe if probe.is_a?(Array)
end
resolver
when ::Array
Array.new(value, context:, filepath:)
Array.new(value, context:, filepath:, ref_resolver: self)
when ::NilClass
nil
else
Simple.new(value)
Simple.new(value, ref_resolver: self)
end
end

def file_at(filepath, file_pointer)
file_contents = file_loader.load(filepath)
value = Hana::Pointer.new(file_pointer).eval(file_contents)
self.for(value, filepath: filepath, context: file_contents)
end

# @visibility private
module Diggable
def dig(*keys)
Expand All @@ -42,10 +54,11 @@ def dig(*keys)

# @visibility private
module Resolvable
def initialize(value, context: value, filepath: nil)
def initialize(value, ref_resolver:, context: value, filepath: nil)
@value = value
@context = context
@filepath = filepath
@ref_resolver = ref_resolver
@dir = if filepath
File.dirname(File.absolute_path(filepath))
else
Expand All @@ -62,6 +75,8 @@ def initialize(value, context: value, filepath: nil)

attr_reader :filepath

private attr_reader :ref_resolver

def ==(_other)
raise "Don't call == on an unresolved value. Use .value == other instead."
end
Expand All @@ -71,16 +86,14 @@ def resolve_ref(pointer)
value = Hana::Pointer.new(pointer[1..]).eval(context)
raise "Unknown reference #{pointer} in #{context}" unless value

return RefResolver.for(value, filepath:, context:)
return ref_resolver.for(value, filepath:, context:)
end

relative_path, file_pointer = pointer.split('#')
full_path = File.expand_path(relative_path, dir)
return RefResolver.load(full_path) unless file_pointer
return ref_resolver.load(full_path) unless file_pointer

file_contents = FileLoader.load(full_path)
value = Hana::Pointer.new(file_pointer).eval(file_contents)
RefResolver.for(value, filepath: full_path, context: file_contents)
ref_resolver.file_at(full_path, file_pointer)
rescue OpenapiFirst::FileNotFoundError => e
message = "Problem with reference resolving #{pointer.inspect} in " \
"file #{File.absolute_path(filepath).inspect}: #{e.message}"
Expand Down Expand Up @@ -114,13 +127,13 @@ def resolved
def [](key)
return resolve_ref(@value['$ref'])[key] if !@value.key?(key) && @value.key?('$ref')

RefResolver.for(@value[key], filepath:, context:)
ref_resolver.for(@value[key], filepath:, context:)
end

def fetch(key)
return resolve_ref(@value['$ref']).fetch(key) if !@value.key?(key) && @value.key?('$ref')

RefResolver.for(@value.fetch(key), filepath:, context:)
ref_resolver.for(@value.fetch(key), filepath:, context:)
end

def each
Expand Down Expand Up @@ -170,7 +183,7 @@ def [](index)
item = @value[index]
return resolve_ref(item['$ref']) if item.is_a?(::Hash) && item.key?('$ref')

RefResolver.for(item, filepath:, context:)
ref_resolver.for(item, filepath:, context:)
end

def each
Expand Down
2 changes: 1 addition & 1 deletion lib/openapi_first/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module OpenapiFirst
VERSION = '3.3.0'
VERSION = '3.3.1'
end
2 changes: 1 addition & 1 deletion openapi_first.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
spec.files = Dir['{lib}/**/*.rb', 'LICENSE.txt', 'README.md', 'CHANGELOG.md']
spec.require_paths = ['lib']

spec.required_ruby_version = '>= 3.2.0'
spec.required_ruby_version = '>= 3.3.0'

spec.add_dependency 'drb', '~> 2.0'
spec.add_dependency 'hana', '~> 1.3'
Expand Down
9 changes: 0 additions & 9 deletions spec/openapi_first_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,6 @@
end
end

describe '.clear_cache!' do
it 'clears the file cache so files are reloaded on next load' do
first = OpenapiFirst.load(spec_path)
OpenapiFirst.clear_cache!
second = OpenapiFirst.load(spec_path)
expect(second).not_to be(first)
end
end

describe '.load' do
begin
require 'multi_json'
Expand Down
Loading
Loading