class Scorpio::ResourceBase

Attributes

attributes[R]
options[R]

Public Class Methods

all_schema_properties() click to toggle source
# File lib/scorpio/resource_base.rb, line 153
def all_schema_properties
  represented_schemas.map(&:described_object_property_names).inject(Set.new, &:|)
end
call_operation(operation, call_params: nil, model_attributes: nil) click to toggle source
# File lib/scorpio/resource_base.rb, line 261
def call_operation(operation, call_params: nil, model_attributes: nil)
  call_params = JSI.stringify_symbol_keys(call_params) if call_params.respond_to?(:to_hash)
  model_attributes = JSI.stringify_symbol_keys(model_attributes || {})

  request = Scorpio::Request.new(operation)

  accessor_overridden = -> (accessor) do
    # an accessor is overridden if the default accessor getter (UnboundMethod) is the same
    # as the UnboundMethod returned from instance_method on the owner of that instance method.
    # gotta be the owner since different classes return different UnboundMethod instances for
    # the same method. for example, referring to models of scorpio/test/blog_scorpio_models.rb
    # with the server_variables instance method:
    #    Article.instance_method(:server_variables)
    #    => #<UnboundMethod: #<Class:Article>#server_variables>
    # returns a different UnboundMethod than
    #    Scorpio::ResourceBase.instance_method(:server_variables)
    #    => #<UnboundMethod: #<Class:Scorpio::ResourceBase>#server_variables>
    # even though they are really the same method (the #owner for both is Scorpio::ResourceBase)
    inheritable_accessor_defaults[accessor] != self.singleton_class.instance_method(accessor).owner.instance_method(accessor)
  end

  # pretty ugly... may find a better way to do this.
  request.base_url =        self.base_url        if accessor_overridden.(:base_url)
  request.server_variables = self.server_variables if accessor_overridden.(:server_variables)
  request.server =          self.server          if accessor_overridden.(:server)
  request.user_agent =      self.user_agent      if accessor_overridden.(:user_agent)
  request.faraday_builder = self.faraday_builder if accessor_overridden.(:faraday_builder)
  request.faraday_adapter = self.faraday_adapter if accessor_overridden.(:faraday_adapter)

  request.path_params = request.path_template.variables.map do |var|
    if call_params.respond_to?(:to_hash) && call_params.key?(var)
      {var => call_params[var]}
    elsif model_attributes.respond_to?(:to_hash) && model_attributes.key?(var)
      {var => model_attributes[var]}
    else
      {}
    end
  end.inject({}, &:update)

  # assume that call_params must be included somewhere. model_attributes are a source of required things
  # but not required to be here.
  if call_params.respond_to?(:to_hash)
    unused_call_params = call_params.reject { |k, _| request.path_template.variables.include?(k) }
    if !unused_call_params.empty?
      other_params = unused_call_params
    else
      other_params = nil
    end
  else
    other_params = call_params
  end

  if operation.request_schema
    # TODO deal with model_attributes / call_params better in nested whatever
    if call_params.nil?
      request.body_object = request_body_for_schema(model_attributes, operation.request_schema)
    elsif call_params.respond_to?(:to_hash)
      body = request_body_for_schema(model_attributes.merge(call_params), operation.request_schema)
      request.body_object = body.merge(call_params) # TODO
    else
      request.body_object = call_params
    end
  else
    if other_params
      if METHODS_WITH_BODIES.any? { |m| m.to_s == operation.http_method.downcase.to_s }
        request.body_object = other_params
      else
        if other_params.respond_to?(:to_hash)
          # TODO pay more attention to 'parameters' api method attribute
          request.query_params = other_params
        else
          raise
        end
      end
    end
  end

  ur = request.run_ur

  ur.raise_on_http_error

  initialize_options = {
    'persisted' => true,
    'source' => {'operationId' => operation.operationId, 'call_params' => call_params, 'url' => ur.request.uri.to_s},
    'ur' => ur,
  }
  response_object_to_instances(ur.response.body_object, initialize_options)
end
define_inheritable_accessor(accessor, default_value: nil, default_getter: -> { default_value } click to toggle source

@param accessor [String, Symbol] the name of the accessor @param default_getter [#to_proc] a proc to provide a default value when no value

has been explicitly set

@param default_value [Object] a default value to return when no value has been

explicitly set. do not pass both :default_getter and :default_value.

@param on_set [#to_proc] callback proc, invoked when a value is assigned

# File lib/scorpio/resource_base.rb, line 20
def define_inheritable_accessor(accessor, default_value: nil, default_getter: -> { default_value }, on_set: nil)
  # the value before the field is set (overwritten) is the result of the default_getter proc
  define_singleton_method(accessor, &default_getter)
  inheritable_accessor_defaults[accessor] = self.singleton_class.instance_method(accessor)
  # field setter method. redefines the getter, replacing the method with one that returns the
  # setter's argument (that being inherited to the scope of the define_method(accessor) block
  define_singleton_method(:"#{accessor}=") do |value|
    # the setter operates on the singleton class of the receiver (self)
    singleton_class.instance_exec(value, self) do |value_, klass|
      # remove a previous getter. NameError is raised if a getter is not defined on this class;
      # this may be ignored.
      begin
        remove_method(accessor)
      rescue NameError
      end
      # getter method
      define_method(accessor) { value_ }
      # invoke on_set callback defined on the class
      if on_set
        klass.instance_exec(&on_set)
      end
    end
  end
end
method_names_by_operation() click to toggle source
# File lib/scorpio/resource_base.rb, line 211
def method_names_by_operation
  @method_names_by_operation ||= Hash.new do |h, operation|
    h[operation] = begin
      raise(ArgumentError, operation.pretty_inspect) unless operation.is_a?(Scorpio::OpenAPI::Operation)

      # if Pet is the Scorpio resource class
      # and Pet.tag_name is "pet"
      # and operation's operationId is "pet.add"
      # then the operation's method name on Pet will be "add".
      # if the operationId is just "addPet"
      # then the operation's method name on Pet will be "addPet".
      tag_name_match = tag_name &&
        operation.tags.respond_to?(:to_ary) && # TODO maybe operation.tags.valid?
        operation.tags.include?(tag_name) &&
        operation.operationId &&
        operation.operationId.match(/\A#{Regexp.escape(tag_name)}\.(\w+)\z/)

      if tag_name_match
        method_name = tag_name_match[1]
      else
        method_name = operation.operationId
      end
    end
  end
end
new(attributes = {}, options = {}) click to toggle source
# File lib/scorpio/resource_base.rb, line 461
def initialize(attributes = {}, options = {})
  @attributes = JSI.stringify_symbol_keys(attributes)
  @options = JSI.stringify_symbol_keys(options)
  @persisted = !!@options['persisted']
end
openapi_document() click to toggle source

the openapi document

# File lib/scorpio/resource_base.rb, line 93
def openapi_document
  nil
end
openapi_document=(openapi_document) click to toggle source
# File lib/scorpio/resource_base.rb, line 100
def openapi_document=(openapi_document)
  openapi_document = OpenAPI::Document.from_instance(openapi_document)

  begin
    singleton_class.instance_exec { remove_method(:openapi_document) }
  rescue NameError
  end
  begin
    singleton_class.instance_exec { remove_method(:openapi_document_class) }
  rescue NameError
  end
  openapi_document_class = self
  define_singleton_method(:openapi_document) { openapi_document }
  define_singleton_method(:openapi_document_class) { openapi_document_class }
  define_singleton_method(:openapi_document=) do |_|
    if self == openapi_document_class
      raise(ArgumentError, "openapi_document may only be set once on #{self.inspect}")
    else
      raise(ArgumentError, "openapi_document may not be overridden on subclass #{self.inspect} after it was set on #{openapi_document_class.inspect}")
    end
  end
  # TODO blame validate openapi_document
  update_dynamic_methods
end
openapi_document_class() click to toggle source
# File lib/scorpio/resource_base.rb, line 96
def openapi_document_class
  nil
end
operation_for_resource_class?(operation) click to toggle source
# File lib/scorpio/resource_base.rb, line 172
def operation_for_resource_class?(operation)
  return false unless tag_name

  return true if operation.tags.respond_to?(:to_ary) && operation.tags.include?(tag_name)

  if (operation.request_schemas || []).any? { |s| represented_schemas.include?(s) }
    return true
  end

  return false
end
operation_for_resource_instance?(operation) click to toggle source
# File lib/scorpio/resource_base.rb, line 184
def operation_for_resource_instance?(operation)
  return false unless operation_for_resource_class?(operation)

  # define an instance method if the request schema is for this model
  request_resource_is_self = operation.request_schemas.any? do |request_schema|
    represented_schemas.include?(request_schema)
  end

  # also define an instance method depending on certain attributes the request description
  # might have in common with the model's schema attributes
  request_attributes = []
  # if the path has attributes in common with model schema attributes, we'll define on
  # instance method
  request_attributes |= operation.path_template.variables
  # TODO if the method request schema has attributes in common with the model schema attributes,
  # should we define an instance method?
  #request_attributes |= request_schema && request_schema['type'] == 'object' && request_schema['properties'] ?
  #  request_schema['properties'].keys : []
  # TODO if the method parameters have attributes in common with the model schema attributes,
  # should we define an instance method?
  #request_attributes |= method_desc['parameters'] ? method_desc['parameters'].keys : []

  schema_attributes = represented_schemas.map(&:described_object_property_names).inject(Set.new, &:|)

  return request_resource_is_self || (request_attributes & schema_attributes.to_a).any?
end
request_body_for_schema(object, schema) click to toggle source
# File lib/scorpio/resource_base.rb, line 350
def request_body_for_schema(object, schema)
  if object.is_a?(Scorpio::ResourceBase)
    # TODO request_schema_fail unless schema is for given model type
    request_body_for_schema(object.attributes, schema)
  elsif object.is_a?(JSI::PathedNode)
    request_body_for_schema(object.node_content, schema)
  else
    if object.respond_to?(:to_hash)
      object.map do |key, value|
        if schema
          if schema['type'] == 'object'
            # TODO code dup with response_object_to_instances
            if schema['properties'].respond_to?(:to_hash) && schema['properties'].key?(key)
              subschema = schema['properties'][key]
              include_pair = true
            else
              if schema['patternProperties'].respond_to?(:to_hash)
                _, pattern_schema = schema['patternProperties'].detect do |pattern, _|
                  key =~ Regexp.new(pattern) # TODO map pattern to ruby syntax
                end
              end
              if pattern_schema
                subschema = pattern_schema
                include_pair = true
              else
                if schema['additionalProperties'] == false
                  include_pair = false
                elsif [nil, true].include?(schema['additionalProperties'])
                  include_pair = true
                  subschema = nil
                else
                  include_pair = true
                  subschema = schema['additionalProperties']
                end
              end
            end
          elsif schema['type']
            request_schema_fail(object, schema)
          else
            # TODO not sure
            include_pair = true
            subschema = nil
          end
        end
        if include_pair
          {key => request_body_for_schema(value, subschema)}
        else
          {}
        end
      end.inject({}, &:update)
    elsif object.respond_to?(:to_ary) || object.is_a?(Set)
      object.map do |el|
        if schema
          if schema['type'] == 'array'
            # TODO index based subschema or whatever else works for array
            subschema = schema['items']
          elsif schema['type']
            request_schema_fail(object, schema)
          end
        end
        request_body_for_schema(el, subschema)
      end
    else
      # TODO maybe raise on anything not serializable
      # TODO check conformance to schema, request_schema_fail if not
      object
    end
  end
end
request_schema_fail(object, schema) click to toggle source
# File lib/scorpio/resource_base.rb, line 420
def request_schema_fail(object, schema)
  # TODO blame
end
response_object_to_instances(object, initialize_options = {}) click to toggle source
# File lib/scorpio/resource_base.rb, line 424
def response_object_to_instances(object, initialize_options = {})
  if object.is_a?(JSI::Base)
    models = object.jsi_schemas.map { |schema| models_by_schema[schema] }
    if models.size == 0
      model = nil
    elsif models.size == 1
      model = models.first
    else
      raise(Scorpio::OpenAPI::Error, "multiple models indicated by response JSI. models: #{models.inspect}; jsi: #{jsi.pretty_inspect.chomp}")
    end
  end

  if object.respond_to?(:to_hash)
    out = JSI::Typelike.modified_copy(object) do |_object|
      mod = object.map do |key, value|
        {key => response_object_to_instances(value, initialize_options)}
      end.inject({}, &:update)
      mod = mod.node_content if mod.is_a?(JSI::PathedNode)
      mod
    end
    if model
      model.new(out, initialize_options)
    else
      out
    end
  elsif object.respond_to?(:to_ary)
    JSI::Typelike.modified_copy(object) do
      object.map do |element|
        response_object_to_instances(element, initialize_options)
      end
    end
  else
    object
  end
end
tag_name() click to toggle source
# File lib/scorpio/resource_base.rb, line 125
def tag_name
  nil
end
tag_name=(tag_name) click to toggle source
# File lib/scorpio/resource_base.rb, line 129
def tag_name=(tag_name)
  unless tag_name.respond_to?(:to_str)
    raise(TypeError, "tag_name must be a string; got: #{tag_name.inspect}")
  end
  tag_name = tag_name.to_str

  begin
    singleton_class.instance_exec { remove_method(:tag_name) }
  rescue NameError
  end
  define_singleton_method(:tag_name) { tag_name }
  define_singleton_method(:tag_name=) do |tag_name|
    unless tag_name == self.tag_name
      raise(ArgumentError, "tag_name may not be overridden (to #{tag_name.inspect}). it is been set to #{self.tag_name.inspect}")
    end
  end
  update_dynamic_methods
end
update_class_and_instance_api_methods() click to toggle source
# File lib/scorpio/resource_base.rb, line 237
def update_class_and_instance_api_methods
  openapi_document.paths.each do |path, path_item|
    path_item.each do |http_method, operation|
      next unless operation.is_a?(Scorpio::OpenAPI::Operation)
      method_name = method_names_by_operation[operation]
      if method_name
        # class method
        if operation_for_resource_class?(operation) && !respond_to?(method_name)
          define_singleton_method(method_name) do |call_params = nil|
            call_operation(operation, call_params: call_params)
          end
        end

        # instance method
        if operation_for_resource_instance?(operation) && !method_defined?(method_name)
          define_method(method_name) do |call_params = nil|
            call_operation(operation, call_params: call_params)
          end
        end
      end
    end
  end
end
update_dynamic_methods() click to toggle source
# File lib/scorpio/resource_base.rb, line 148
def update_dynamic_methods
  update_class_and_instance_api_methods
  update_instance_accessors
end
update_instance_accessors() click to toggle source
# File lib/scorpio/resource_base.rb, line 157
def update_instance_accessors
  all_schema_properties.each do |property_name|
    unless method_defined?(property_name)
      define_method(property_name) do
        self[property_name]
      end
    end
    unless method_defined?(:"#{property_name}=")
      define_method(:"#{property_name}=") do |value|
        self[property_name] = value
      end
    end
  end
end

Public Instance Methods

[](key) click to toggle source
# File lib/scorpio/resource_base.rb, line 474
def [](key)
  @attributes[key]
end
[]=(key, value) click to toggle source
# File lib/scorpio/resource_base.rb, line 478
def []=(key, value)
  @attributes[key] = value
end
as_json(*opt) click to toggle source
# File lib/scorpio/resource_base.rb, line 508
def as_json(*opt)
  JSI::Typelike.as_json(@attributes, *opt)
end
call_api_method(method_name, call_params: nil) click to toggle source
# File lib/scorpio/resource_base.rb, line 482
def call_api_method(method_name, call_params: nil)
  operation = self.class.method_names_by_operation.invert[method_name] || raise(ArgumentError)
  call_operation(operation, call_params: call_params)
end
call_operation(operation, call_params: nil) click to toggle source
# File lib/scorpio/resource_base.rb, line 487
def call_operation(operation, call_params: nil)
  response = self.class.call_operation(operation, call_params: call_params, model_attributes: self.attributes)

  # if we're making a POST or PUT and the request schema is this resource, we'll assume that
  # the request is persisting this resource
  request_resource_is_self = operation.request_schema && self.class.represented_schemas.include?(operation.request_schema)
  if @options['ur'].is_a?(Scorpio::Ur)
    response_schema = @options['ur'].response.response_schema
  end
  response_resource_is_self = response_schema && self.class.represented_schemas.include?(response_schema)
  if request_resource_is_self && %w(put post).include?(operation.http_method.to_s.downcase)
    @persisted = true

    if response_resource_is_self
      @attributes = response.attributes
    end
  end

  response
end
inspect() click to toggle source
# File lib/scorpio/resource_base.rb, line 512
def inspect
  "\#<#{self.class.inspect} #{attributes.inspect}>"
end
jsi_fingerprint() click to toggle source
# File lib/scorpio/resource_base.rb, line 529
def jsi_fingerprint
  {class: self.class, attributes: JSI::Typelike.as_json(@attributes)}
end
persisted?() click to toggle source
# File lib/scorpio/resource_base.rb, line 470
def persisted?
  @persisted
end
pretty_print(q) click to toggle source
# File lib/scorpio/resource_base.rb, line 515
def pretty_print(q)
  q.instance_exec(self) do |obj|
    text "\#<#{obj.class.inspect}"
    group_sub {
      nest(2) {
        breakable ' '
        pp obj.attributes
      }
    }
    breakable ''
    text '>'
  end
end