remi / her

Her is an ORM (Object Relational Mapper) that maps REST resources to Ruby objects. It is designed to build applications that are powered by a RESTful API instead of a database.
MIT License
2.05k stars 322 forks source link

Add middleware for parsing HAL & Collection+JSON? #48

Closed mattiassvedhem closed 11 years ago

mattiassvedhem commented 11 years ago

Would it be a good idea to include parsers for hypermedia types?

remi commented 11 years ago

I’m not sure what you mean by that. Do you have any example of that kind of data?

mattiassvedhem commented 11 years ago

Of course.

HAL is a hypermedia type built on top of JSON and XML. It adds semantics for specifying links and embedded data to represent a resource in a HATEOAS way. The resource should be a representation of the current state of the application and provide the client with actions to progress the state.

Roar has built in parsing support for application/hal+json & application/hal+xml although I don't think they are following Mike Kelly's spec 100%. We're using it to create a Hypermedia driven API for our application. The benefits as I see it is that it creates a more loose coupling between client and server as you don't need to construct the urls on the client.

So basically it would boil down to creating a parser middleware for application/hal+json. Then you might consider to include it in the library. I'll see what I can come up with.

If you are curious about Hypermedia API's you might want to check out Steve's book or his talks on the subject.

remi commented 11 years ago

Sorry, I completely forgot about this issue. If you want to create a new middleware that handles application/hal+json response I would gladly add it!

remi commented 11 years ago

I’m going to close this issue since there’s no more activity here.

ekampp commented 9 years ago

@remiprev I'm in the position that I'd love to use Her for a project. The backend delivers HAL JSON, so I need to develop a middleware to parse the JSON.

HAL formats the JSON like this:

{
  "_embedded": {
    "users": [
      {
        "id": 1,
        ...
      },
      ...
    ]
  }

Can you guide me to where in the Her project I should map the name of the class calling the endpoint, and the nesting in the JSON?

Update

I have managed to parse the JSON, but I need to change the key, with which the associations are bound to the object, e.g. User.find(1).articles should look in the JSON key [:_embedded][:articles], not just [:articles].

Update

I have been able to pinpoint where associated collection are being parsed. Adding this bit of code seems to allow me to pass the [:_embedded] to the new_collection method.

elsif parsed_data[:data].is_a?(Hash) && hal_format?
  new_collection(parsed_data)

And adding this bit of code to the attributes allows me to correctly pull out the association in the [:_embedded] part of the HAL JSON.

if request_data[:data].is_a?(Hash) && hal_format?
  request_data[:data][:_embedded][root_element]

I'm defining the hal_format? like this, right now, in an initializer, simply to get going. This will be moved somewhere more appropriate at some point.

module Her
  module Model
    module HTTP
      module ClassMethods
        def hal_format?
          true
        end
      end
    end
  end
end

module Her
  module Model
    module Parse
      module ClassMethods
        def hal_format?
          true
        end
      end
    end
  end
end

The issue is now, that I'm getting a stack level to deep. This is – as you know – notoriously hard to debug, so any help here is appreciated.

Inspecting the generated collection via new_collection(parsed_data).tap { |c| puts c.inspect } gives me a (seemingly) correct array of objects. But does this trigger another run at the get_raw method that I enhanced at the very beginning?

# => [#<Article(articles/349) title="a cool article", ..., id=349>, ...]

What is the upstream caller of the get method, when accessing an association? E.i. User.find(1).articles.all, the one using the aforementioned collection.

Thanks in advance.

AwakenTheJaken commented 7 years ago

I hate to dig up an old thread, but in my search for a solution to this I stumbled across it and I wanted to share what I learned. What I did was create a Faraday Response Middleware and parsed the data like this:

class Akeneo::Parser < Faraday::Response::Middleware
  def on_complete(env)
    json = MultiJson.load(env[:body])
      env[:body] = {
        data: json['_links'].blank? ? json : json['_embedded']['items'],
        errors: json[:errors]
      }
  end
end

It isn't the most robust solution as it is only checking if the response contains ['_links'] but it works for my needs. I hope this will help someone else with a similar issue or even lead to a better solution!