Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
80770fd
Added parsed model spec Hash as constant
bogdanadrianmarc Mar 11, 2026
abed4a1
Refactored code into final path helper method
bogdanadrianmarc Mar 11, 2026
6cee246
Added to_underscore method to prevent rails require
bogdanadrianmarc Mar 11, 2026
60eeade
Added helper method for parsing model file
bogdanadrianmarc Mar 11, 2026
54261b9
Added type2fulltype helper method for parsing model file
bogdanadrianmarc Mar 11, 2026
474ae6e
Added method to parse model spec file
bogdanadrianmarc Mar 11, 2026
5c09502
Parse model spec as last init step
bogdanadrianmarc Mar 11, 2026
df92385
Refactored code into helper method
bogdanadrianmarc Mar 12, 2026
ddeb421
Fixed small rubocop error
bogdanadrianmarc Mar 12, 2026
755399e
Added method to check if property is in model spec
bogdanadrianmarc Mar 12, 2026
a48ddea
Respond to missing if property is in model spec as well
bogdanadrianmarc Mar 12, 2026
b1ab9b0
If property is in model spec return nil instead of error raise
bogdanadrianmarc Mar 12, 2026
317fc6d
Refactored method to satisfy rubocop
bogdanadrianmarc Mar 12, 2026
fa9f494
Fixed more rubocop issues
bogdanadrianmarc Mar 12, 2026
851e579
Rescue and print error message if parsing the model failed
bogdanadrianmarc Mar 13, 2026
474d413
Added missing model spec for cbd api fixture
bogdanadrianmarc Mar 13, 2026
f9057c7
Added unit test for parsing the model spec file
bogdanadrianmarc Mar 13, 2026
a0b776a
Added minitest-stub-const gem to gemspec
bogdanadrianmarc Mar 13, 2026
962e9bc
Require new gem in test helper
bogdanadrianmarc Mar 13, 2026
e4d8c3f
Added test to make sure nil is returned if property is not in the dat…
bogdanadrianmarc Mar 13, 2026
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
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ GEM
builder
minitest (>= 5.0)
ruby-progressbar
minitest-stub-const (0.6)
mocha (2.7.1)
ruby2_keywords (>= 0.0.5)
multipart-post (2.4.1)
Expand Down Expand Up @@ -89,6 +90,7 @@ DEPENDENCIES
byebug (~> 11.1.3)
minitest (~> 5.25)
minitest-reporters (~> 1.7)
minitest-stub-const (~> 0.6)
mocha (~> 2.4)
rake (~> 13.0.1)
rubocop (~> 1.26.0)
Expand Down
119 changes: 114 additions & 5 deletions lib/sapi_client/application.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# frozen-string-literal: true
# frozen_string_literal: true

module SapiClient
# Wraps an entire Sapi-NT application, such that we can walk over all of the
# enclosed endpoint specifications to perform various operations, such as creating
# methods we can call
class Application
class Application # rubocop:disable Metrics/ClassLength
PARSED_MODEL_SPEC = {} # rubocop:disable Style/MutableConstant

def initialize(base_url, application_or_endpoints)
unless File.exist?(application_or_endpoints)
raise(SapiError, "Could not find spec file/directory #{application_or_endpoints}")
Expand All @@ -16,6 +18,9 @@ def initialize(base_url, application_or_endpoints)
@specification = (@application_spec_file && YAML.load_file(application_or_endpoints)) || {
'sapi-nt' => { 'config' => { 'loadSpecPath' => 'classpath:endpointSpecs' } }
}

# Call method to parse model spec before returning
parse_model_spec
end

attr_reader :base_url, :specification
Expand All @@ -36,14 +41,18 @@ def load_spec_path
@endpoints_path || configuration['loadSpecPath'].sub(/^classpath:/, '')
end

def endpoint_group_files
def final_path
if @endpoints_path.nil?
Dir["#{application_spec_dir}/#{load_spec_path}/*.yaml"]
"#{application_spec_dir}/#{load_spec_path}/*.yaml"
else
Dir["#{@endpoints_path}/*.yaml"]
"#{@endpoints_path}/*.yaml"
end
end

def endpoint_group_files
Dir[final_path]
end

def endpoints
endpoint_group_files
.map { |spec| SapiClient::EndpointGroup.new(base_url, spec) }
Expand Down Expand Up @@ -97,5 +106,105 @@ def get_hierarchy_proc(endpoint, inst)
inst.get_hierarchy(endpoint_url, options, scheme)
end
end

# Parses the API model spec file and populates Hash with resulting class names and properties
def parse_model_spec # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
# Load model spec file
model_spec = final_path.gsub('/*.yaml', '/model.yaml')
m = YAML.load_file(model_spec)

# Parse class names and prefixes
qname2local = {}
m['classes'].each do |cls|
qname2local[cls['class']] = cls['name']
end
prefix2uri = m['prefixes']
builtins = {
'http://www.w3.org/1999/02/22-rdf-syntax-ns#langString' => 'String',
'http://www.w3.org/2001/XMLSchema#string' => 'String',
'http://www.w3.org/2000/01/rdf-schema#Literal' => 'String',
'http://www.w3.org/2001/XMLSchema#boolean' => 'bool',
'http://www.w3.org/2001/XMLSchema#date' => 'Date',
'http://www.w3.org/2001/XMLSchema#dateTime' => 'DateTime',
'http://www.w3.org/2001/XMLSchema#integer' => 'Integer',
'http://www.w3.org/2001/XMLSchema#decimal' => 'BigDecimal',
'http://www.w3.org/2001/XMLSchema#double' => 'Float'
}

# Parse classes and properties and populate PARSED_MODEL_SPEC
m['classes'].each do |cls|
# Skip if class has already been parsed
next if PARSED_MODEL_SPEC.keys.include?(type2fulltype(cls['class'], prefix2uri))

# If not, parse class and properties
PARSED_MODEL_SPEC[type2fulltype(cls['class'], prefix2uri)] = {}
cls['properties'].each do |prop|
ts = Set.new

if prop['type'].is_a?(Array)
prop['type'].each do |t|
ts << type2ruby(t, prefix2uri, qname2local, builtins)
end
else
ts << type2ruby(prop['type'], prefix2uri, qname2local, builtins)
end
ts << 'nil' if prop['optional']

PARSED_MODEL_SPEC[type2fulltype(cls['class'], prefix2uri)][prop['name']] = returns(ts)
snake_prop = to_underscore(prop['name'])
if snake_prop != prop['name']
PARSED_MODEL_SPEC[type2fulltype(cls['class'], prefix2uri)][snake_prop] =
returns(ts)
end
end
end

nil
rescue StandardError => e
puts(SapiError, "Error parsing model spec file #{model_spec}: #{e.message}")
end

# Helper method for parsing model spec file
def type2fulltype(typ, prefix2uri)
spl = typ.split(':')
pref = prefix2uri[spl[0]]
pref + spl[1]
rescue StandardError
typ
end

# Helper method for parsing model spec file
def type2ruby(typ, prefix2uri, qname2local, builtins)
full_uri = type2fulltype(typ, prefix2uri)
if qname2local.include? typ
qname2local[typ]
elsif builtins.include? full_uri
builtins[full_uri]
else
'String'
end
rescue StandardError
'String'
end

# Helper method for parsing model spec file
def returns(types)
if types.size > 1
"( #{types.join(' | ')} )"
elsif types.size == 1
types.first
else
'untyped'
end
end

# Helper method for parsing model spec file
def to_underscore(string)
string.gsub('::', '/')
.gsub(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
.tr('-', '_')
.downcase
end
end
end
32 changes: 27 additions & 5 deletions lib/sapi_client/sapi_resource.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# frozen-string-literal: true
# frozen_string_literal: true

module SapiClient
# Encapsulates a JSON-LD -style resource that we get back from a Sapi-NT endpoint,
Expand Down Expand Up @@ -92,7 +92,7 @@ def types

# @return True if this resource has the given URI among its types
def type?(uri)
type_uris = types&.map { |typ| typ.is_a?(String) ? typ : typ['@id'] }
type_uris = types&.map { |typ| type_to_string(typ) }
type_uris&.include?(uri)
end

Expand Down Expand Up @@ -134,14 +134,21 @@ def name(options = {})
end

def respond_to_missing?(property, _include_private = false)
resource.key?(property) || resource.key?(as_camel_case_method_name(property))
resource.key?(property) ||
resource.key?(as_camel_case_method_name(property)) ||
property_in_model_spec?(property) ||
property_in_model_spec?(as_camel_case_method_name(property))
end

def method_missing(property, *_args)
return self[property] if resource.key?(property)
# If the property is not found, check if it's in the model spec for this resource's type(s)
return nil if property_in_model_spec?(property)

# If still not found, try looking for a camelCase version of the property as well
cc_property = as_camel_case_method_name(property)
return self[cc_property] if resource.key?(cc_property)
return nil if property_in_model_spec?(cc_property)

super
end
Expand Down Expand Up @@ -193,9 +200,10 @@ def lang_select(values, preferred_lang)
# Return the given value as an un-wrapped resource. A Hash given to this
# method will have its keys transformed to symbols.
def as_resource(res)
if res.is_a?(SapiResource) # rubocop:disable Style/CaseLikeIf
case res
when SapiResource
res.resource.clone
elsif res.is_a?(Hash)
when Hash
hash_with_symbol_keys(res)
else
{ '@id': res.to_s }
Expand Down Expand Up @@ -240,5 +248,19 @@ def as_camel_case_method_name(str)
first_segment, *remaining_segments = str.to_s.split('_')
[first_segment, *remaining_segments.map(&:capitalize)].join.to_sym
end

# Helper method to convert type to string
def type_to_string(typ)
typ.is_a?(String) ? typ : typ['@id']
end

# Helper method to find if property is in PARSED_MODEL_SPEC for the corresponding type
def property_in_model_spec?(property)
types&.any? do |typ|
full_type = type_to_string(typ)
Application::PARSED_MODEL_SPEC.key?(full_type) &&
Application::PARSED_MODEL_SPEC[full_type].key?(property.to_s)
end
end
end
end
1 change: 1 addition & 0 deletions sapi-client-ruby.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'byebug', '~> 11.1.3'
spec.add_development_dependency 'minitest', '~> 5.25'
spec.add_development_dependency 'minitest-reporters', '~> 1.7'
spec.add_development_dependency 'minitest-stub-const', '~> 0.6'
spec.add_development_dependency 'mocha', '~> 2.4'
spec.add_development_dependency 'rake', '~> 13.0.1'
spec.add_development_dependency 'rubocop', '~> 1.26.0'
Expand Down
Loading
Loading