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

Allow naming of root element when rendering response as XML? #398

Open
Ben-Fenner opened this issue Jan 13, 2022 · 19 comments
Open

Allow naming of root element when rendering response as XML? #398

Ben-Fenner opened this issue Jan 13, 2022 · 19 comments

Comments

@Ben-Fenner
Copy link

Ben-Fenner commented Jan 13, 2022

We have a client who insists on our API providing XML, so we're fulfilling that.

The XML root element name is hard-coded to data in the Graphiti renderer which is not ideal since there is a child element inside also named data.
We have worked around this, but it would be nice to be able to specify the root element name where the Graphiti renderer could use it.

Looking at the source, here is where the magic happens, best I can tell:

render(self.class.hash_renderer(@proxy)).to_xml(root: :data)

Right now this is a simplified version of what our work-around looks like:

# app/controllers/api/my_controller.rb

class Api::MyController < Api::ApplicationController
  def index
    resources = MyResource.all(params)

    respond_to do |format|
      format.json    { render(json:    resources) }
      format.jsonapi { render(jsonapi: resources) }
      format.xml     { render(xml:     pretty_xml(resources)) }
    end
  end

  # By default when a Graphiti resource proxy list is resolved to XML it looks like this:
  #
  #  <data>
  #    <data type="array">
  #      <datum>
  #        ...
  #      </datum>
  #    </data>
  #    <meta>
  #      ...
  #    </meta>
  #  </data>
  #
  #
  # Instead we want it to look like this:
  #
  #  <root>
  #    <data type="array">
  #      <datum>
  #        ...
  #      </datum>
  #    </data>
  #    <meta>
  #      ...
  #    </meta>
  #  </root>
  def pretty_xml(resource_proxy)
    json = resource_proxy.to_json
    JSON.parse(json).to_xml(root: :root)
  end
end
@richmolj
Copy link
Contributor

Not totally against it, but JSONAPI top-level key is data. Isn't it the same issue? Intent is to be consistent.

@Ben-Fenner
Copy link
Author

Ben-Fenner commented Jan 13, 2022

I've been developing and testing this whole time using JSON (not JSONAPI) and then am only just now getting to work on the XML part. I haven't actually paid any attention to what the JSONAPI results look like.

Looking at it now, it seems there is no top-level key for the JSONAPI payload (same as with the JSON payload). There are two keys on equal level just like with JSON. The data key and the meta key.

(Although maybe things have changed in newer versions. I am stuck on Graphti 1.3.5 right now. Sorry!)

Example:

{"data":[{"id":"1","type":"my_resource","attributes":{"title":"Resource number 1"},{"id":"2","type":"my_resource","attributes":{"title":"Resource number 2"}],"meta":{"stats":{"total":{"count":4}}}}

When no count is requested, the meta key still exists in a JSONAPI response (while the JSON response omits it) but is just an empty hash.

It would appear that XML requires an additional, named, root layer, whereas the JSON and JSONAPI formats are happy enough to have an unnamed root container layer.

@richmolj
Copy link
Contributor

The other keys are meta/included, which aren't really relevant to the shape of JSON/XML, which leaves us with just data. Isn't the top-level key for JSON responses data right now? Again not opposed to it, but the consistency outweights a key named data in my personal opinion

@Ben-Fenner
Copy link
Author

Ben-Fenner commented Jan 13, 2022

As far as I can tell, there is no top-level key for JSON-formatted payloads, nor for JSONAPI-formatted payloads. And by that I mean, if a meta key exists, it is at the same level as the data key. When they both exist, there is no top-level key. It's just a hash with two equal-level keys. The top-level in this situation is essentially the hash itself, which JSON is happy to leave unnamed.

XML just wants to wrap that whole situation up into an additional named top-level key. So when there is only a data key, you end up with two data keys (one the parent of the other) as shown in the code comment in the original post.

Am I explaining this well enough? Maybe I need to draw some Venn diagrams? :)

@Ben-Fenner
Copy link
Author

Ben-Fenner commented Jan 13, 2022

Maybe if I format the example from above a bit you'll see what I mean.
Does this representation of the JSONAPI response help?

{ "data":[
           {"id":"1","type":"my_resource","attributes":{"title":"Resource number 1"},
           {"id":"2","type":"my_resource","attributes":{"title":"Resource number 2"}
         ]
  ,
  "meta":{"stats":{"total":{"count":4}}}
}

@richmolj
Copy link
Contributor

Right, this is JSONAPI. But meta isn't relevant for JSON/XML (included is the other example). Put another way, vanilla JSON (not JSONAPI) has a top-level data key, XML matches that same payload for consistency.

@Ben-Fenner
Copy link
Author

Ben-Fenner commented Jan 13, 2022

Hmmm, I'm not seeing that on my end.

Here is what the same response looks like when requested as JSON (and returned as JSON not JSONAPI):

{ "data":[
           {"id":"1","title":"Resource number 1"},
           {"id":"2","title":"Resource number 2"}
         ]
  ,
  "meta":{"stats":{"total":{"count":4}}}
}

@richmolj
Copy link
Contributor

Haha, my mistake I didn't know we supported meta. Sorry! In any case, doesn't this match XML and JSONAPI? You want to rename data, right?

@Ben-Fenner
Copy link
Author

Ben-Fenner commented Jan 13, 2022

JSON and JSONAPI match each other when it comes down to the relevant hierarchy. XML adds an additional layer wrapping everything. That additional wrapper just so happens to be called data. It could be called root and make a bit more sense, and be more like what XML consumers are expecting. But I mean, it could be called master_of_the_universe and still not have any effect on the data key you're hoping to keep unchanged.

To be clear, I am not trying to rename the data key that is on the same level as the meta and included keys. Of course, that should remain untouched. This additional root-level wrapper that only exists for the XML representation is what I want to rename (and am doing so with my ugly work-around). It would be nice to not have to use this work-around.

Again, check out the large code comment above for what I'm hoping to accomplish.

@richmolj
Copy link
Contributor

Gotcha, I see now, thanks for bearing with me. Sounds like we want to remove that top-level key entirely then?

@Ben-Fenner
Copy link
Author

Ben-Fenner commented Jan 13, 2022

I'm not well-versed in XML, so I've looked this up. Apparently a root (top-level key) is required. So it can not be removed.
https://stackoverflow.com/questions/4304038/do-you-always-have-to-have-a-root-node-with-xml-xsd

The ideal situation IMO is to default the name of the top-level key to data (as to not mess things up for anyone else currently relying on this gem and its existing behavior) but allow it to be changed if desired.
Something like this:

def to_xml(root = :data)
  render(self.class.hash_renderer(@proxy)).to_xml(root: root)
end

How that would be handled upstream of the method is not something I've looked at, nor have I considered exactly what it would look like to send this new argument in the controller...

@richmolj
Copy link
Contributor

Ah, I thought there was a root: false but apparently not. I think you've won me over :)

@Ben-Fenner
Copy link
Author

Ben-Fenner commented Jan 13, 2022

Thank you so much for your quick and informative replies. I keep forgetting to mention this. It's nice to have a good discussion with quick turn-around. 😁

If you're cool with potentially breaking implementations that rely on the XML formatting done by Graphiti, your easy button is:

def to_xml
  render(self.class.hash_renderer(@proxy)).to_xml(root: :root)
end

Or if you're okay with the default name of hash you can omit the setting entirely:

def to_xml
  render(self.class.hash_renderer(@proxy)).to_xml
end

Update your test(s) and be done. 😈

I'm not sure this is the way you'd like to go, but it is a thought. 😉

@richmolj
Copy link
Contributor

No problem! Thanks for bearing with me. I was thinking for backwards-compat:

def to_xml
  render(self.class.hash_renderer(@proxy)).to_xml(root: Graphiti.config.xml_root)
end

And defaulting the option to :root

@Ben-Fenner
Copy link
Author

Ben-Fenner commented Jan 13, 2022

Oh yah, a configuration seems like an elegant solution. For backwards-compatibility you'd want to default that option to :data though, right? Just in case someone's API spits out XML right now that's consumed by someone else who expects a top-level wrapper of <data></data>.

@richmolj
Copy link
Contributor

Oh yes my mistake, you got it :)

@Ben-Fenner
Copy link
Author

Ben-Fenner commented Jan 13, 2022

This config file is the /.graphiticfg.yml file, correct?
The one with the namespace: setting? (That is the only setting I've ever seen mentioned for that file BTW.)

And that file always seems to be shown as beginning with 3 hyphens. Is that right?
Ours looks like this right now since we're not versioning our API:

---
namespace: /

Are those hyphens really necessary? I've always been curious about that...

@richmolj
Copy link
Contributor

That's really only used for generators, check out https://github.com/graphiti-api/graphiti/blob/master/lib/graphiti/configuration.rb

@Ben-Fenner
Copy link
Author

Ben-Fenner commented Jan 13, 2022

To be clear, the namespace: setting is really only used for generators (which is what I'd gathered from the docs) but the config file itself is potentially used for other configuration, including this new xml_root: configuration if it comes to pass?

Edit: I see looking at the code that it does appear that namespace: is currently the only configuration setting possible, and it dictates where the schema.json is expected to be...

Edit 2: But then there is mention of a Graphiti.config.schema_path setting in a raised exception message. So, I guess I've taken us way far off topic. Excuse me for that. I'll stop pulling at threads now. 😏

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

No branches or pull requests

2 participants