Class: OpenStax::Api::RepresentableSchemaPrinter

Inherits:
Object
  • Object
show all
Defined in:
lib/openstax/api/representable_schema_printer.rb

Class Method Summary collapse

Class Method Details

.definition_name(name) ⇒ Object

Gets the definition name for the given representer name



36
37
38
# File 'lib/openstax/api/representable_schema_printer.rb', line 36

def self.definition_name(name)
  "#/definitions/#{name}"
end

.json(representer, options = {}) ⇒ Object

Returns some formatted Markdown with HTML containing the JSON schema for a given representer



15
16
17
18
19
20
21
22
23
24
# File 'lib/openstax/api/representable_schema_printer.rb', line 15

def self.json(representer, options={})
  options[:include] ||= [:readable, :writeable]

  schema = json_schema(representer, options)

  json_string = JSON.pretty_generate(schema)

  "\n## Schema  {##{SecureRandom.hex(4)} .schema}\n" +
  "\n<pre class='code'>\n#{json_string}\n</pre>\n"
end

.json_object(representer, definitions, options = {}) ⇒ Object

Helper function for json_schema



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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/openstax/api/representable_schema_printer.rb', line 41

def self.json_object(representer, definitions, options = {})
  # Initialize the schema
  schema = { type: :object, required: [], properties: {}, additionalProperties: false }

  representer.representable_attrs.each do |attr|
    # Handle some common attributes (as, schema_info, required)
    name = attr[:as].blank? ?
             attr[:name] :
             attr[:as].evaluate(representer) rescue attr[:as].call rescue attr[:name]
    schema_info = attr[:schema_info] || {}

    schema[:required].push(name.to_sym) if schema_info[:required]

    # Skip attr if it does not have the the specified key set to false
    # or it is explicitly "required"
    next unless [options[:include]].flatten.any? do |inc|
      m = inc.to_s + "?"
      (attr.respond_to?(m) ? attr.send(m) : attr[inc]) != false
    end || schema_info[:required]

    # Guess a default type based on the attribute name
    type = attr[:type].blank? ? (name.end_with?('id') ? :integer : :string) :
                                attr[:type].to_s.downcase
    attr_info ||= { type: type }

    # Process the schema_info attribute
    schema_info.each do |key, value|
      # Handle special keys (required, definitions, type, enum)
      case key
      when :required
        next
      when :definitions
        definitions.merge!(value)
        next
      when :type
        next if attr_info[:type].nil?
      when :enum
        attr_info.delete(:type)
      end

      # Handle Procs and Uber::Callables
      case value
      when Proc
        value = representer.instance_exec &value
      when Uber::Callable
        value = value.call(representer)
      end

      # Store the schema_info k-v pair in attr_info
      attr_info[key] = value
    end

    # Overwrite type for collections
    attr_info[:type] = 'array' if attr[:collection]

    # Handle nested representers
    if attr[:extend]
      # The nested representer can be either a simple representer or
      # an Uber::Callable, in which case there could be multiple representers

      # Get the nested representers
      # Evaluate syntax is evaluate(context, instance or class, *args)
      # If we have no instance or class (since we have no fragment), we pass Object
      # By convention, our callables should return an array of all possible
      # representers when we pass the all_sub_representers: true option
      instance = attr[:instance].evaluate(representer, {}) rescue nil
      klass = attr[:class].evaluate(representer, {}) rescue Object
      decorators = [attr[:extend].evaluate(
        representer, instance || klass, all_sub_representers: true
      )].flatten.compact rescue []

      # Count the representers
      include_oneof = decorators.length > 1
      # If we have more than one possible representer, use oneOf to list them all
      sreps = include_oneof ? { oneOf: [] } : {}

      decorators.each do |decorator|
        # Attempt to get the representer's name
        rname = representer_name(decorator)

        if rname
          # We have a representer name, so use a schema definition with that name
          dname = definition_name(rname)
          dhash = { :$ref => dname }

          include_oneof ? sreps[:oneOf].push(dhash) : sreps = dhash

          if definitions[rname].nil?
            # No definition with that name found, so add it
            # A blank definition is added first to prevent infinite loops
            definitions[rname] = {}
            definitions[rname] = json_object(decorator, definitions, options)
          end
        else
          # No representer name, so add the object inline instead
          obj = json_object(decorator, definitions, options)
          include_oneof ? sreps[:oneOf].push(obj) : sreps = obj
        end
      end

      if attr[:collection]
        # Collection
        #   Type already set above (array)
        #   Add sub representers under :items
        attr_info[:items] = sreps
      else
        # Not a collection
        #   Type included in ref
        #   Add sub representers inline
        attr_info.delete(:type)
        attr_info.merge!(sreps)
      end

    end

    # Merge attr_info back into the schema
    schema[:properties][name.to_sym] = attr_info
  end

  # Cleanup unused fields
  [:required, :properties].each do |field|
    schema.delete(field) if schema[field].blank?
  end

  # Return the completed object schema
  schema
end

.json_schema(representer, options = {}) ⇒ Object

Returns the json schema for a representer



6
7
8
9
10
11
# File 'lib/openstax/api/representable_schema_printer.rb', line 6

def self.json_schema(representer, options = {})
  definitions = {}
  schema = json_object(representer, definitions, options)
  schema[:definitions] = definitions unless definitions.blank?
  schema
end

.representer_name(representer) ⇒ Object

Attempts to extract the given representer’s name



29
30
31
32
33
# File 'lib/openstax/api/representable_schema_printer.rb', line 29

def self.representer_name(representer)
  name = representer.try :name
  return nil if name.nil?
  name.chomp('Representer').demodulize.camelize(:lower)
end