Class: Aspera::Schema::Registry

Inherits:
Object
  • Object
show all
Includes:
Singleton
Defined in:
lib/aspera/schema/registry.rb

Constant Summary collapse

LOCATIONS =
{
  spec:         'aspera/transfer/spec.schema.yaml',
  args:         'aspera/sync/args.schema.yaml',
  conf:         'aspera/sync/conf.schema.yaml',
  opts:         'aspera/cli/options.schema.yaml',
  aoc:          'aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml',
  automation:   'aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml',
  faspex:       'aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml',
  faspio:       'aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml',
  console:      'aspera/schema/IBM Aspera Console-enhanced.yaml',
  node:         'aspera/schema/IBM Aspera Node API-4.4.6.yaml',
  shares:       'aspera/schema/IBM_Aspera_Shares.yaml',
  async_tables: 'aspera/schema/async_tables.yaml'
}
OWNED =

Schemas owned by ascli: values are validated against them (vendor API schemas are validated by the API)

i[spec args conf opts async_tables].freeze
OPTIONS =
'opts'
TRANSFER_SPEC =
'spec'
SYNC_CONF =
'conf'
SYNC_ARGS =
'args'
AOC =
'aoc'
AUTOMATION =
'automation'
FASPEX =
'faspex'
FASPIO =
'faspio'
CONSOLE =
'console'
NODE =
'node'
SHARES =
'shares+/api/v1'
ASYNC_TABLES =
'async_tables'
LOG_OPTIONS =
"#{OPTIONS}:components.schemas.LogOptions"
DIRECT_AGENT_OPTIONS =
"#{OPTIONS}:components.schemas.DirectAgentOptions"
NODE_AGENT_OPTIONS =
"#{OPTIONS}:components.schemas.NodeAgentOptions"
HTTPGW_AGENT_OPTIONS =
"#{OPTIONS}:components.schemas.HttpgwAgentOptions"
TRANSFERD_AGENT_OPTIONS =
"#{OPTIONS}:components.schemas.TransferdAgentOptions"
TRANSFER_AGENT_OPTIONS =
"#{OPTIONS}:components.schemas.TransferAgentOptions"
SMTP_OPTIONS =
"#{OPTIONS}:components.schemas.SmtpOptions"
HTTP_OPTIONS =
"#{OPTIONS}:components.schemas.HttpOptions"
VAULT_OPTIONS =
"#{OPTIONS}:components.schemas.VaultOptions"
VAULT_SECRET =
"#{OPTIONS}:components.schemas.VaultSecret"
IMAGE_OPTIONS =
"#{OPTIONS}:components.schemas.ImageOptions"
OUT_OPTIONS =
"#{OPTIONS}:components.schemas.OutOptions"
PACKAGE_FOLDER_OPTIONS =
"#{OPTIONS}:components.schemas.PackageFolderOptions"
REQ_BODY =
'.requestBody.content.application/json.schema'
QUERY_PARAMS_SUFFIX =

Suffix appended to a dotted path to signal query-param extraction in reader()

'.parameters'

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize ⇒ Registry

Returns a new instance of Registry.



104
105
106
107
# File 'lib/aspera/schema/registry.rb', line 104

def initialize
  @cache = {}
  @main_folder = File.expand_path('../..', __dir__)
end

Class Method Details

.instance ⇒ Registry

Returns the singleton instance of Registry

Returns:



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/aspera/schema/registry.rb', line 13

class Registry
  include Singleton

  class << self
    def known?(sym)
      LOCATIONS.key?(sym)
    end

    # @param name_path [String] schema path, e.g. `opts:components.schemas.HttpOptions`
    # @return [Boolean] `true` if schema is owned by ascli (not a vendor API)
    def owned?(name_path)
      OWNED.include?(name_path.split(':', 2).first.to_sym)
    end

    # Split a component string into registry key and optional path prefix.
    # Syntax: 'key' or 'key+/prefix' (e.g. 'shares+/api/v1')
    # @param component [String] registry key with optional '+/prefix'
    # @return [Array(String, String)] [key, prefix] where prefix may be ''
    def split_component(component)
      key, prefix = component.split('+', 2)
      [key, prefix || '']
    end

    # Get path to request body schema, no check if it exists
    # @param component [String] registry key, optionally with path prefix (e.g. 'shares+/api/v1')
    # @param endpoint  [String] endpoint path without leading slash (e.g. 'data/shares.post')
    # @return [String] schema path usable in schema: keyword
    def req_body(component, endpoint)
      key, prefix = split_component(component)
      "#{key}:paths.#{prefix}/#{endpoint}.requestBody.content.application/json.schema"
    end

    # Get path to query parameters for a GET endpoint
    # @param component [String] registry key, optionally with path prefix (e.g. 'shares+/api/v1')
    # @param endpoint  [String] resource path without leading slash (e.g. 'data/shares')
    # @param method    [String] HTTP method (default: 'get')
    # @return [String] schema path usable in query_schema: keyword
    def query_params(component, endpoint, method: 'get')
      key, prefix = split_component(component)
      "#{key}:paths.#{prefix}/#{endpoint}.#{method}#{QUERY_PARAMS_SUFFIX}"
    end
  end

  LOCATIONS = {
    spec:         'aspera/transfer/spec.schema.yaml',
    args:         'aspera/sync/args.schema.yaml',
    conf:         'aspera/sync/conf.schema.yaml',
    opts:         'aspera/cli/options.schema.yaml',
    aoc:          'aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml',
    automation:   'aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml',
    faspex:       'aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml',
    faspio:       'aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml',
    console:      'aspera/schema/IBM Aspera Console-enhanced.yaml',
    node:         'aspera/schema/IBM Aspera Node API-4.4.6.yaml',
    shares:       'aspera/schema/IBM_Aspera_Shares.yaml',
    async_tables: 'aspera/schema/async_tables.yaml'
  }

  # Schemas owned by ascli: values are validated against them (vendor API schemas are validated by the API)
  OWNED = i[spec args conf opts async_tables].freeze

  OPTIONS = 'opts'
  TRANSFER_SPEC = 'spec'
  SYNC_CONF = 'conf'
  SYNC_ARGS = 'args'
  AOC = 'aoc'
  AUTOMATION = 'automation'
  FASPEX = 'faspex'
  FASPIO = 'faspio'
  CONSOLE = 'console'
  NODE = 'node'
  SHARES = 'shares+/api/v1'
  ASYNC_TABLES = 'async_tables'
  LOG_OPTIONS             = "#{OPTIONS}:components.schemas.LogOptions"
  DIRECT_AGENT_OPTIONS    = "#{OPTIONS}:components.schemas.DirectAgentOptions"
  NODE_AGENT_OPTIONS      = "#{OPTIONS}:components.schemas.NodeAgentOptions"
  HTTPGW_AGENT_OPTIONS    = "#{OPTIONS}:components.schemas.HttpgwAgentOptions"
  TRANSFERD_AGENT_OPTIONS = "#{OPTIONS}:components.schemas.TransferdAgentOptions"
  TRANSFER_AGENT_OPTIONS  = "#{OPTIONS}:components.schemas.TransferAgentOptions"
  SMTP_OPTIONS            = "#{OPTIONS}:components.schemas.SmtpOptions"
  HTTP_OPTIONS            = "#{OPTIONS}:components.schemas.HttpOptions"
  VAULT_OPTIONS           = "#{OPTIONS}:components.schemas.VaultOptions"
  VAULT_SECRET            = "#{OPTIONS}:components.schemas.VaultSecret"
  IMAGE_OPTIONS           = "#{OPTIONS}:components.schemas.ImageOptions"
  OUT_OPTIONS             = "#{OPTIONS}:components.schemas.OutOptions"
  PACKAGE_FOLDER_OPTIONS  = "#{OPTIONS}:components.schemas.PackageFolderOptions"

  REQ_BODY = '.requestBody.content.application/json.schema'
  # Suffix appended to a dotted path to signal query-param extraction in reader()
  QUERY_PARAMS_SUFFIX = '.parameters'

  def initialize
    @cache = {}
    @main_folder = File.expand_path('../..', __dir__)
  end

  # Read schema from file or from cache.
  # When name_path ends with QUERY_PARAMS_SUFFIX, the OAS `parameters` array at that path
  # is synthesised into an object schema via Reader.from_query_params instead of navigating
  # into the tree.
  # @param name_path [String] registry key with optional colon-separated dotted path suffix,
  #   e.g. "faspex:paths./packages.get.parameters" or "faspex:paths./packages.post.requestBody..."
  # @return [Reader] schema reader
  def reader(name_path)
    name, path = name_path.split(':', 2)
    sym = name.to_sym
    Aspera.assert(Registry.known?(sym)) { "schema: #{sym}" }
    spec_file = File.join(@main_folder, LOCATIONS[sym])
    @cache[sym] = Yaml.safe_load(File.read(spec_file)) if spec_file.end_with?('.yaml') && !@cache.key?(sym)
    @cache[sym] = JSON.parse(File.read(spec_file)) if spec_file.end_with?('.json') && !@cache.key?(sym)
    # Query-params path: strip the suffix, navigate to the operation node, extract parameters
    if path&.end_with?(QUERY_PARAMS_SUFFIX)
      parent_path = path.delete_suffix(QUERY_PARAMS_SUFFIX)
      node = @cache[sym].dig(*parent_path.split('.'))
      # Parameters may be references to components.parameters
      params = (node&.fetch('parameters', []) || []).map do |param|
        param.key?('$ref') ? @cache[sym].dig(*param['$ref'].delete_prefix('#/').split('/')) : param
      end
      return Reader.from_query_params(params.compact)
    end
    reader = Reader.new(@cache[sym])
    return reader unless path
    reader.dig(*path.split('.'))
  end
end

.known?(sym) ⇒ Boolean

Returns:

  • (Boolean)


17
18
19
# File 'lib/aspera/schema/registry.rb', line 17

def known?(sym)
  LOCATIONS.key?(sym)
end

.owned?(name_path) ⇒ Boolean

Returns true if schema is owned by ascli (not a vendor API).

Parameters:

  • name_path (String) —

    schema path, e.g. opts:components.schemas.HttpOptions

Returns:

  • (Boolean) —

    true if schema is owned by ascli (not a vendor API)



23
24
25
# File 'lib/aspera/schema/registry.rb', line 23

def owned?(name_path)
  OWNED.include?(name_path.split(':', 2).first.to_sym)
end

.query_params(component, endpoint, method: 'get') ⇒ String

Get path to query parameters for a GET endpoint

Parameters:

  • component (String) —

    registry key, optionally with path prefix (e.g. 'shares+/api/v1')

  • endpoint (String) —

    resource path without leading slash (e.g. 'data/shares')

  • method (String) (defaults to: 'get') —

    HTTP method (default: 'get')

Returns:

  • (String) —

    schema path usable in query_schema: keyword



50
51
52
53
# File 'lib/aspera/schema/registry.rb', line 50

def query_params(component, endpoint, method: 'get')
  key, prefix = split_component(component)
  "#{key}:paths.#{prefix}/#{endpoint}.#{method}#{QUERY_PARAMS_SUFFIX}"
end

.req_body(component, endpoint) ⇒ String

Get path to request body schema, no check if it exists

Parameters:

  • component (String) —

    registry key, optionally with path prefix (e.g. 'shares+/api/v1')

  • endpoint (String) —

    endpoint path without leading slash (e.g. 'data/shares.post')

Returns:

  • (String) —

    schema path usable in schema: keyword



40
41
42
43
# File 'lib/aspera/schema/registry.rb', line 40

def req_body(component, endpoint)
  key, prefix = split_component(component)
  "#{key}:paths.#{prefix}/#{endpoint}.requestBody.content.application/json.schema"
end

.split_component(component) ⇒ Array(String, String)

Split a component string into registry key and optional path prefix. Syntax: 'key' or 'key+/prefix' (e.g. 'shares+/api/v1')

Parameters:

  • component (String) —

    registry key with optional '+/prefix'

Returns:

  • (Array(String, String)) —

    [key, prefix] where prefix may be ''



31
32
33
34
# File 'lib/aspera/schema/registry.rb', line 31

def split_component(component)
  key, prefix = component.split('+', 2)
  [key, prefix || '']
end

Instance Method Details

#reader(name_path) ⇒ Reader

Read schema from file or from cache. When name_path ends with QUERY_PARAMS_SUFFIX, the OAS parameters array at that path is synthesised into an object schema via Reader.from_query_params instead of navigating into the tree.

Parameters:

  • name_path (String) —

    registry key with optional colon-separated dotted path suffix, e.g. "faspex:paths./packages.get.parameters" or "faspex:paths./packages.post.requestBody..."

Returns:



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/aspera/schema/registry.rb', line 116

def reader(name_path)
  name, path = name_path.split(':', 2)
  sym = name.to_sym
  Aspera.assert(Registry.known?(sym)) { "schema: #{sym}" }
  spec_file = File.join(@main_folder, LOCATIONS[sym])
  @cache[sym] = Yaml.safe_load(File.read(spec_file)) if spec_file.end_with?('.yaml') && !@cache.key?(sym)
  @cache[sym] = JSON.parse(File.read(spec_file)) if spec_file.end_with?('.json') && !@cache.key?(sym)
  # Query-params path: strip the suffix, navigate to the operation node, extract parameters
  if path&.end_with?(QUERY_PARAMS_SUFFIX)
    parent_path = path.delete_suffix(QUERY_PARAMS_SUFFIX)
    node = @cache[sym].dig(*parent_path.split('.'))
    # Parameters may be references to components.parameters
    params = (node&.fetch('parameters', []) || []).map do |param|
      param.key?('$ref') ? @cache[sym].dig(*param['$ref'].delete_prefix('#/').split('/')) : param
    end
    return Reader.from_query_params(params.compact)
  end
  reader = Reader.new(@cache[sym])
  return reader unless path
  reader.dig(*path.split('.'))
end