Permalink
Browse files

Merge pull request #20 from seomoz/nullable

Add better nullability support.
  • Loading branch information...
2 parents 93e631a + 1928a36 commit 055f617c9eacf042069929406e75b02a301e70c4 @myronmarston myronmarston committed Apr 28, 2013
@@ -1,7 +1,7 @@
+require 'interpol'
require 'interpol/endpoint'
require 'interpol/errors'
require 'yaml'
-require 'interpol/configuration_ruby_18_extensions' if RUBY_VERSION.to_f < 1.9
require 'uri'
module Interpol
@@ -33,8 +33,9 @@ def with_endpoint_matching(method, path)
# Public: Defines interpol configuration.
class Configuration
- attr_reader :endpoint_definition_files, :endpoints, :filter_example_data_blocks
- attr_accessor :validation_mode, :documentation_title, :endpoint_definition_merge_key_files
+ attr_reader :endpoint_definition_files, :endpoints, :filter_example_data_blocks,
+ :endpoint_definition_merge_key_files
+ attr_accessor :validation_mode, :documentation_title
def initialize
self.endpoint_definition_files = []
@@ -48,16 +49,25 @@ def initialize
end
def endpoint_definition_files=(files)
- self.endpoints = files.map do |file|
- Endpoint.new(deserialized_hash_from file)
- end
+ @endpoints = nil
@endpoint_definition_files = files
end
+ def endpoint_definition_merge_key_files=(files)
+ @endpoints = nil
+ @endpoint_definition_merge_key_files = files
+ end
+
def endpoints=(endpoints)
@endpoints = endpoints.extend(DefinitionFinder)
end
+ def endpoints
+ @endpoints ||= @endpoint_definition_files.map do |file|
+ Endpoint.new(deserialized_hash_from(file), self)
+ end.extend(DefinitionFinder)
+ end
+
[:request, :response].each do |type|
class_eval <<-EOEVAL, __FILE__, __LINE__ + 1
def #{type}_version(version = nil, &block)
@@ -156,6 +166,15 @@ def example_response_for(endpoint_def, env)
selector.call(endpoint_def, env)
end
+ def scalars_nullable_by_default=(value)
+ @endpoints = nil
+ @scalars_nullable_by_default = value
+ end
+
+ def scalars_nullable_by_default?
+ @scalars_nullable_by_default
+ end
+
def self.default
@default ||= Configuration.new
end
@@ -183,24 +202,9 @@ def param_parser_for(type, options)
end
private
-
- # 1.9 version
- include Module.new {
- BAD_ALIAS_ERROR = defined?(::Psych::BadAlias) ?
- ::Psych::BadAlias : TypeError
- def deserialized_hash_from(file)
- YAML.load(yaml_content_for file)
- rescue BAD_ALIAS_ERROR => e
- raise ConfigurationError.new \
- "Received an error while loading YAML from #{file}: \"" +
- "#{e.class}: #{e.message}\" If you are using YAML merge keys " +
- "to declare shared types, you must configure endpoint_definition_merge_key_files " +
- "before endpoint_definition_files.", e
- end
- }
-
- # Needed to override deserialized_hash_from for Ruby 1.8
- include Interpol::ConfigurationRuby18Extensions if RUBY_VERSION.to_f < 1.9
+ def deserialized_hash_from(file)
+ YAML.load(yaml_content_for file)
+ end
def yaml_content_for(file)
File.read(file).gsub(/\A---\n/, "---\n" + endpoint_merge_keys + "\n\n")
@@ -1,27 +0,0 @@
-module Interpol
- module ConfigurationRuby18Extensions
- def deserialized_hash_from(file)
- YAML.load(yaml_content_for file).tap do |yaml|
- if bad_class = bad_deserialized_yaml(yaml)
- raise ConfigurationError.new \
- "Received an error while loading YAML from #{file}: \"" +
- "Got object of type: #{bad_class}\"\n If you are using YAML merge keys " +
- "to declare shared types, you must configure endpoint_definition_merge_key_files " +
- "before endpoint_definition_files."
- end
- end
- end
-
- # returns nil if the YAML has been only partially deserialized by Syck
- # and there are YAML::Syck objects.
- def bad_deserialized_yaml(yaml)
- if [Hash, Array].include? yaml.class
- yaml.map { |elem| bad_deserialized_yaml(elem) }.compact.first
- elsif yaml.class.name =~ /YAML::Syck::/
- yaml.class.name # Bad!
- else
- nil
- end
- end
- end
-end
@@ -1,6 +1,7 @@
require 'json-schema'
require 'interpol/errors'
require 'forwardable'
+require 'set'
module JSON
# The JSON-schema namespace
@@ -65,13 +66,14 @@ def fetch_from(hash, key)
# based on the endpoint definitions in the YAML files.
class Endpoint
include HashFetcher
- attr_reader :name, :route, :method, :custom_metadata
+ attr_reader :name, :route, :method, :custom_metadata, :configuration
- def initialize(endpoint_hash)
+ def initialize(endpoint_hash, configuration = Interpol.default_configuration)
@name = fetch_from(endpoint_hash, 'name')
@route = fetch_from(endpoint_hash, 'route')
@method = fetch_from(endpoint_hash, 'method').downcase.to_sym
+ @configuration = configuration
@custom_metadata = endpoint_hash.fetch('meta') { {} }
@definitions_hash, @all_definitions = extract_definitions_from(endpoint_hash)
@@ -181,7 +183,7 @@ class EndpointDefinition
attr_reader :endpoint, :message_type, :version, :schema,
:path_params, :query_params, :examples, :custom_metadata
extend Forwardable
- def_delegators :endpoint, :route
+ def_delegators :endpoint, :route, :configuration
DEFAULT_PARAM_HASH = { 'type' => 'object', 'properties' => {} }
@@ -244,12 +246,14 @@ def make_schema_strict!(raw_schema, modify_object=true)
end
end
- def make_schema_hash_strict!(raw_schema, modify_object=true)
+ def make_schema_hash_strict!(raw_schema, make_this_schema_strict=true)
+ conditionally_make_nullable(raw_schema) if make_this_schema_strict
+
raw_schema.each do |key, value|
make_schema_strict!(value, key != 'properties')
end
- return unless modify_object
+ return unless make_this_schema_strict
if raw_schema.has_key?('properties')
raw_schema['additionalProperties'] ||= false
@@ -258,9 +262,33 @@ def make_schema_hash_strict!(raw_schema, modify_object=true)
raw_schema['required'] = !raw_schema.delete('optional')
end
- def make_schema_array_strict!(raw_schema, modify_object=true)
+ def make_schema_array_strict!(raw_schema, make_nested_schemas_strict=true)
raw_schema.each do |entry|
- make_schema_strict!(entry, modify_object)
+ make_schema_strict!(entry, make_nested_schemas_strict)
+ end
+ end
+
+ def conditionally_make_nullable(raw_schema)
+ return unless should_be_nullable?(raw_schema)
+
+ types = Array(raw_schema['type'])
+ return if types.none? || types.include?('null')
+
+ raw_schema['type'] = (types << "null")
+ end
+
+ def should_be_nullable?(raw_schema)
+ raw_schema.fetch('nullable') do
+ configuration.scalars_nullable_by_default? && scalar?(raw_schema)
+ end
+ end
+
+ NON_SCALAR_TYPES = %w[ object array ]
+ def scalar?(raw_schema)
+ types = Array(raw_schema['type']).to_set
+
+ NON_SCALAR_TYPES.none? do |non_scalar|
+ types.include?(non_scalar)
end
end
@@ -150,6 +150,28 @@ def find_with_status_code(status_code, options)
expect(config.endpoints.map(&:name)).to match_array %w[ project_list task_list ]
end
+ it 'passes itself to the endpoints as the configuration' do
+ write_file "#{dir}/e1.yml", endpoint_definition_yml
+ config.endpoint_definition_files = Dir["#{dir}/*.yml"]
+ endpoint_config = config.endpoints.first.configuration
+ expect(endpoint_config).to be(config)
+ end
+
+ it 'allows `scalars_nullable_by_default` to be configured after ' +
+ '`endpoint_definition_files`' do
+ write_file "#{dir}/e1.yml", endpoint_definition_yml
+
+ config.endpoint_definition_files = Dir["#{dir}/*.yml"]
+ config.endpoints # to force it to load
+ config.scalars_nullable_by_default = true
+
+ endpoint_def = config.endpoints.first.definitions.first
+
+ expect {
+ endpoint_def.validate_data!('name' => nil)
+ }.not_to raise_error
+ end
+
context "when YAML merge keys are used" do
let_without_indentation(:types) do <<-EOF
---
@@ -196,18 +218,18 @@ def assert_expected_endpoint
assert_expected_endpoint
end
+ it 'supports the merge keys when configured after the endpoint definition files' do
+ config.endpoint_definition_files = Dir["#{dir}/e1.yml"]
+ config.endpoint_definition_merge_key_files = Dir["#{dir}/merge_keys.yml"]
+ assert_expected_endpoint
+ end
+
it 'works when the merge key YAML file lacks the leading `---`' do
write_file "#{dir}/merge_keys.yml", types.gsub(/\A---\n/, '')
config.endpoint_definition_merge_key_files = Dir["#{dir}/merge_keys.yml"]
config.endpoint_definition_files = Dir["#{dir}/e1.yml"]
assert_expected_endpoint
end
-
- it 'raises a helpful error when endpoint_definition_files is configured first' do
- expect {
- config.endpoint_definition_files = Dir["#{dir}/e1.yml"]
- }.to raise_error(/endpoint_definition_merge_key_files/)
- end
end
it 'is memoized' do
@@ -291,6 +313,17 @@ def assert_expected_endpoint
end
end
+ describe "#scalars_nullable_by_default?" do
+ it 'defaults to false' do
+ expect(config.scalars_nullable_by_default?).to be_false
+ end
+
+ it 'can be set to true' do
+ config.scalars_nullable_by_default = true
+ expect(config.scalars_nullable_by_default?).to be_true
+ end
+ end
+
describe "#api_version" do
before { config.stub(:warn) }
Oops, something went wrong. Retry.

0 comments on commit 055f617

Please sign in to comment.