Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve handling of fragments #382

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open

Conversation

notEthan
Copy link
Contributor

I've encountered a few problems dealing with fragments that this PR addresses:

  • traversing the pointer through the document tries to instantiate the value as a json schema at every level, which is not valid when the value is an array, and not needed in any case. only the final value pointed to needs to be instantiated as a schema.
  • when validate_schema is on, it validates the given json document, not the schema at the pointer. it should not assume that the given value is a json schema, only that the value at the pointer is.
  • strings containing escaped values, either URI escaped in the fragment, or escaped with ~0 / ~1 as defined in https://tools.ietf.org/html/rfc6901 are not handled correctly.

I made a class JSON::Schema::Pointer to abstract the parsing and evaluation of pointers, and updated other code around this.

@@ -6,6 +6,8 @@ class Schema
attr_accessor :schema, :uri, :validator

def initialize(schema,uri,parent_validator=nil)
raise ArgumentError, "schema must be a hash; got: #{schema.inspect}" unless schema.is_a?(Hash)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this change is kind of unrelated, but it results in a better error message when an array is passed as the schema argument (as the previous fragment-handling code was prone to do) - before the error message would end up being TypeError: no implicit conversion of String into Integer when trying to call @schema['id'] below, which is rather obscure.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying (slowly) to move the codebase away from explicit type checking towards duck typing. Could you please alter this check from schema.is_a?(Hash) to schema.respond_to?(:to_hash), and the line below from @schema = schema to @schema = schema.to_hash?

# If the :fragment option is set, try and validate against the fragment
if opts[:fragment]
@base_schema = schema_from_fragment(@base_schema, opts[:fragment])
end
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is moved before the validate_schema block so that the correct schema, at the fragment pointer, is validated

if match
parse_pointer(match.post_match)
else
raise(PointerSyntaxError, "Invalid fragment syntax in #{fragment.inspect}: fragment must begin with #")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've also tried to improve the information given in these error messages, as the previous "Invalid fragment resolution for :fragment option" was not very useful to debug

end

def schema_from_fragment(base_schema, fragment)
schema_uri = base_schema.uri
fragments = fragment.split("/")
Copy link
Contributor Author

@notEthan notEthan May 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another fix I just noticed: this should be split("/", -1); as it is empty string keys would be mishandled. #/foo// should parse to fragments ['foo', '', ''] and in json {"foo": {"": {"": 7}}} should evaluate to 7.

@notEthan
Copy link
Contributor Author

added #383 and rebased this against it

@notEthan
Copy link
Contributor Author

this thing alive?

@iainbeeston
Copy link
Contributor

@notEthan Sorry, I've been immensely busy at work and at home for several weeks. I'll take a proper look at this once things calm down here

@notEthan
Copy link
Contributor Author

sounds good, thanks

Copy link
Contributor

@iainbeeston iainbeeston left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this. It seems to be mostly refactoring with a few targetted changes?

I've requested a few small changes. Could you also please add those bug-fixes to the CHANGELOG.md? (Ideally in the same commits as the code changes they refer to)

I believe there are other places in the code where we use pointers but I'm happy to update those later myself (unless you have time to do it yourself, in which case I'd be very grateful!)

@@ -6,6 +6,8 @@ class Schema
attr_accessor :schema, :uri, :validator

def initialize(schema,uri,parent_validator=nil)
raise ArgumentError, "schema must be a hash; got: #{schema.inspect}" unless schema.is_a?(Hash)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying (slowly) to move the codebase away from explicit type checking towards duck typing. Could you please alter this check from schema.is_a?(Hash) to schema.respond_to?(:to_hash), and the line below from @schema = schema to @schema = schema.to_hash?

# - :reference_tokens - the representation is an array of tokens referencing a path in a document
def initialize(type, representation)
@type = type
if type == :reference_tokens
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't see anywhere where this is used. Is it needed? (And if not we can remove the last clause in representation_s too)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used it in code that I have been working on that relies on this Pointer class - I use the array of reference tokens there. dealing with the string (pointer or fragment) in code is less useful than the tokens. I'd like this to be part of the API of this class.

"#<#{self.class.name} #{@type} = #{representation_s}>"
end

def representation_s
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we make this and the methods below private, to remove them from the public api of the class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made this method private but #pointer and #fragment should be public. I've added method documents for those.

# - :fragment - the representation is a fragment containing a pointer (starting with #)
# - :pointer - the representation is a pointer (starting with /)
# - :reference_tokens - the representation is an array of tokens referencing a path in a document
def initialize(type, representation)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, the more I think about this the more I feel that this aught to be refactored from a single Pointer class, into a Pointer parent class and a subclass for each type (eg. FragmentPointer and AbsolutePointer maybe?). Right now most instance methods work differently based on the type, and it feels like an instance of the Switch Statements Smell, where polymorphism would work better.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started down this path but on further reflection I think I disagree. the class represents the same sort of pointer no matter what the external representation it came from. the only method that is any different, once the external representation has been parsed, is #to_s and that is only to keep track of where it came from - it's not really a part of what the Pointer is, just something I found useful for debugging.

@iainbeeston
Copy link
Contributor

You might need to rebase this, as I've merged a fragment-related bugfix (#217)

@notEthan
Copy link
Contributor Author

oh my, what a long time I have neglected this. back to it. rebased and addressed some comments.

@notEthan
Copy link
Contributor Author

if you agree that a Pointer is a Pointer, I hope this is ready to be merged. if you feel strongly that it should be polymorphic, I can do that.

@notEthan
Copy link
Contributor Author

hmm. test failures on jruby that I'm not getting locally. one looks related to this change, others don't. will address what I can.

@notEthan notEthan force-pushed the fragments branch 3 times, most recently from 3a6d994 to 127e482 Compare March 13, 2018 02:24
…e schema of the root is incorrect; it is correct to validate the schema located at the fragment.
…rs that must be escaped in json pointer syntax
… dealing with pointers in fragments or in plain pointer syntax. use this in #schema_from_fragment.
@notEthan
Copy link
Contributor Author

I put a commit fixing a require that was missing, causing failures on jruby, ahead of the content of this PR, it can be merged together or separately

@notEthan
Copy link
Contributor Author

ping on this, hopefully you have time to look at it and we can bring it home

notEthan added a commit to notEthan/scorpio that referenced this pull request Apr 7, 2018
notEthan added a commit to notEthan/scorpio that referenced this pull request Apr 8, 2018
notEthan added a commit to notEthan/scorpio that referenced this pull request Apr 11, 2018
notEthan added a commit to notEthan/scorpio that referenced this pull request Apr 12, 2018
@skryukov
Copy link

Hi @iainbeeston thanks for the great gem! Could you please look at this PR again? Keys with slashes are all over the place in the swagger specs =)

@scalp42
Copy link

scalp42 commented Jun 3, 2019

@iainbeeston is it still maintened? Would it be possible to add more contributors as a lot of PRs are sitting? Thanks!

@iainbeeston
Copy link
Contributor

@scalp42 I'm afraid that the gem does not have any active maintainers at the moment. If you're interested in becoming one please comment on #423

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants