blakechambers / matterhorn Goto Github PK
View Code? Open in Web Editor NEWLicense: MIT License
License: MIT License
Using JSON API as guide from http://jsonapi.org/format/#crud-updating to
PATCH
method. PATCH /posts/1
PUT
links
is NOT supported in 0.1.0 i.e. http://jsonapi.org/format/#crud-updating-to-many-relationships'Errors. Errors returned from server will be keyed with a top level "errors" object. Each error should provide a "title" per spec for any Matterhorn errors. Later versions of the api should also provide "detail" and "code".'
Example returned error:
{:errors=>
[{:title=>"resource_error", :detail=>"author: can't be blank", :status=>422},
{:title=>"resource_error", :detail=>"body: can't be blank", :status=>422},
{:title=>"resource_error", :detail=>"title: can't be blank", :status=>422}]}
/posts/1,2,3,4,5
/posts/5519bc4368616e50aa070000,5519bc4368616e50aa090000,5519bc4368616e50aa0b0000,5519bc4368616e50aa0d0000.json
/posts/1,2,3,4,5/vote
AS described here: http://jsonapi.org/format/#document-structure-meta
{
"meta": {
"jotd": "Why did the chicken cross the road?"
},
"data": {
// ...
}
}
Add support for https://github.com/blakechambers/matterhorn/blob/master/features/0001-paging_interface.md
This will need to be based off of the links_refactor
branch.
While json api does support sorting, it's version is more specific to providing something flexible for all types of sorting and ordering.
While this is nice, in a production system (and certainly for public apis) locking down the ordering to some named that can be managed server-side is a little more realistic. This becomes more important when using named indices in the database, especially with larger datasets.
That said, we should consider a lighter weight ordering syntax, that can match 1-to-1 with any indexed sets you want to provide.
Here's an example syntax of what's being proposed:
class Article
include Mongoid::Document
include Mongoid::Timestamps
field :title
field :number_of_comments
belongs_to :author
end
class ArticlesController < ApplicationController
include Matterhorn::Ordering
allow_order 'oldest', :created_at.asc
allow_order 'recent', :created_at.desc
# first ordered by number_of_comments, then created_at
allow_order 'most_comments', :number_of_comments.desc, :created_at.desc
default_order 'oldest'
end
allow_order
- creates a named order. In the above example should pass directly to mongoids sorting on collections. Reserve the keyword 'none', in the future we can add order=none to override default_order
default_order
- specifies which order will be chosen when the order param is not requestedOrder should be merged into the request env and passed down the chain until it gets interpreted during serialization.
When using ordering, Links should be added for the different named orders. For example, the above controller should add links as such:
GET /articles.json
{
"orders": {
"oldest" : "http://example.org/articles?order=oldest",
"recent" : "http://example.org/articles?order=recent",
},
...
This should allow sorting to be implemented and used alongside ordering in future versions.
This is mirror ticket of #70, but should focus on the code to add/update/destroy various relationships.
Some additional definition will need to be added to the links controller spec document.
Per: #9
" Each link class should decide if it can be included or not.
Inclusions and links may or may not have a way to display a URI template in the header, so it should be up to the link class to decide the format. In the above example, the links to author and comments only work if the author_id and initial_comments_ids are available in the serialized article. It's not a requirement to have those. "
Does not appear to be working as stated above:
I removed :author_id
from Post serializer and the author is still linked in links
in top level.
raw_body=
{"links"=>
{"author"=>"http://example.org/users/{posts.author_id}",
"votes"=>"http://example.org/posts/{posts._id}/vote",
"comments"=>"http://example.org/posts/{posts.}/comments",
"initial_comments"=>"http://example.org/comments/{posts.initial_comments_ids}"},
"data"=>
{"_id"=>"551c4f8268616e0e1f200000",
"type"=>"post",
"links"=>
{"author"=>{"linkage"=>{"id"=>"551c4f8268616e0e1f1f0000", "type"=>"users"}, "related"=>"http://example.org/users/551c4f8268616e0e1f1f0000"},
"votes"=>{"linkage"=>{"id"=>"551c4f8268616e0e1f200000", "type"=>"votes"}, "related"=>"http://example.org/posts/551c4f8268616e0e1f200000/vote"},
"comments"=>{"linkage"=>{"id"=>"551c4f8268616e0e1f200000", "type"=>"comments"}, "related"=>"http://example.org/posts/551c4f8268616e0e1f200000/comments"},
"initial_comments"=>{"linkage"=>{"id"=>"1,2,3", "type"=>"comments"}, "related"=>"http://example.org/comments/1,2,3"},
"self"=>"http://example.org/posts/551c4f8268616e0e1f200000"},
"_type"=>"post",
"body"=>"Et quibusdam ut sit est aut."}},
For now let's set this as Matterhorn::CONTENT_TYPE = "application/vnd.api+json". In the future it should be part of the configuration.
Implement with http://jsonapi.org/format/#crud-deleting as guide.
DELETE /posts/1
see http://jsonapi.org/format/#document-structure-top-level-links
Given the following routes and models:
#./config/routes.rb
resources :articles
resources :authors
#./app/models
class Author
include Mongoid::Document
end
class Article
include Mongoid::Document
belongs_to :author
end
author = Author.create
article = Article.create author: author
Currently, the API would provide:
GET `http://example.com/articles/1`
{
"links": {
"self": "http://example.com/articles/1",
"author" : "http://example.com/authors/{articles.author_id}"
},
"data": {
"type": "articles",
"id": "1",
"links": {
"author": {
"related" : "http://example.com/authors/1",
"linkage" : {
"type" : "authors",
"id" : 1
}
}
}
}
}
Instead it should provide:
GET `http://example.com/articles/1`
{
"links": {
"self": "http://example.com/articles/1"
},
"data": {
"type": "articles",
"id": "1",
"links": {
"author": {
"related" : "http://example.com/authors/1",
"linkage" : {
"type" : "authors",
"id" : 1
}
}
}
}
}
As it states in the docs, "The top-level links object MAY self links, related links, or pagination links". Currently we provide a fourth item, which is the current resources links (provided as url templates). That needs to be removed and the the logic to build the templates should be removed.
We should support using the top level data
key and type
for our JSON as described in the spec. Currently we're using "_id" and "_type".
Discovered this while refactoring some tests, see the resource/index_spec, specifically the commented sections.
it "should allow a page param" do
request_params.merge! offset: "1"
perform_request!
expect(data).to provide(ordered_posts[1..-1])
end
But this test fails with the following output:
Failure/Error: expect(data).to provide(ordered_posts[0..0])
Actual and Expected do not match.
Actual [{"id"=>"554abf18426c61146a770000", "links"=>{"author"=>{"linkage"=>{"id"=>"554abf18426c61146a760000", "type"=>"users"}, "related"=>"http://example.org/users/554abf18426c61146a760000?limit=1"}, "vote"=>{"linkage"=>{"post_id"=>"554abf18426c61146a770000", "type"=>"votes"}, "related"=>"http://example.org/posts/554abf18426c61146a770000/vote?limit=1"}, "comments"=>{"linkage"=>{"post_id"=>"554abf18426c61146a770000", "type"=>"comments"}, "related"=>"http://example.org/posts/554abf18426c61146a770000/comments?limit=1"}, "self"=>"http://example.org/posts/554abf18426c61146a770000?limit=1"}, "type"=>"posts", "author_id"=>"554abf18426c61146a760000", "body"=>"body", "initial_comments_ids"=>nil}]
Expected [{"id"=>"554abf18426c61146a770000", "links"=>{"author"=>{"linkage"=>{"id"=>"554abf18426c61146a760000", "type"=>"users"}, "related"=>"http://example.org/users/554abf18426c61146a760000"}, "vote"=>{"linkage"=>{"post_id"=>"554abf18426c61146a770000", "type"=>"votes"}, "related"=>"http://example.org/posts/554abf18426c61146a770000/vote"}, "comments"=>{"linkage"=>{"post_id"=>"554abf18426c61146a770000", "type"=>"comments"}, "related"=>"http://example.org/posts/554abf18426c61146a770000/comments"}, "self"=>"http://example.org/posts/554abf18426c61146a770000"}, "type"=>"posts", "author_id"=>"554abf18426c61146a760000", "body"=>"body", "initial_comments_ids"=>nil}]
Params are getting passed into the link building for relationships, which shouldn't happen.
Strictly test "type" field always returns a pluralized name. Currently BaseSerializer returns type as a singular either sometimes or all the time.
Types in links are already pluralized, so we should just ensure that type is pluralized across the board.
Assuming you have a collection of posts, and your controller lists a private inclusion or link available from the controller:
class PostsController < Matterhorn::Base
scope = proc do |set_member|
current_user.votes.all
end
add_inclusion :votes, scope: scope
...
The responding json (by the json-api spec) should list the votes inclusion. The problem with that is when this resource is serialized by another controller, the underlying resource would have different links. This would break the goal of globally cacheable objects.
We should move the logic to where the current_user
object provided at the DSL level from there inclusions could be provided anywhere, model or another class file or confit or sorts. Possibly current_user
is method built in rack and exposed as a thread variable.
In controller, something like expose :current_user
would expose that variable to the scoped serializer as a configuration. This configuration should not be accessible to all the individual serializers but be usable when constructing the links for each individual object.
/posts/1/author
used in links
: <relation>
: related
- Compound Documents http://jsonapi.org/format/#document-structure-compound-documentsSee: https://github.com/blakechambers/matterhorn/blob/master/spec/resources_api/show_spec.rb#L40
Should this not be adjusted to keep API consistent?
I can't recall if this was discussed @blakechambers . But we need to write some matchers in Matterhorn for all the :links, :includes, :linkages,etc if serial-spec is not meant to address JSON API spec directly.
Was the intent to add more functionality to serial-spec?
/cc @Nakort
Matterhorn needs a request_env to store it's request logic. Currently there isn't good separation of concerns between the logic for generating the links inclusions and the actual display of those objects.
This is needed prior to finishing the code for inclusions, which currently don't track the request env at all. Inclusions will need to have access to request_env in order to check for authentication visibility... e.g. if you request an inclusion that requires authentication, the inclusion will need to decide how to handle that (raise a 403, ignore request).
This would include:
In your api app, most code bases should setup the following code:
# app/controllers/application_controller.rb
class ApplicationController < Matterhorn::Base
include AuthenticationCode # this is custom to you're app
helper_method :current_user
add_env :current_user
protected ###################################
def current_user
@current_user ||= begin
User.where(id: session[:user_id]).first
end
end
end
Now within requests:
# app/controllers/posts_controller.rb
# matterhorn index methods should handle the index
# methods for you, this is just to demonstrate that the
# request_env is available as an instance method.
def index
request_env[:current_user] # == current_user
end
When the request_env
is called it should load each of the methods into a memoized hash. If it is not called, current_user shouldn't be built.
# builder_support
def build_json
# this should merge controller.request_env into the base object, to be mixed into options for scoped collection/resource.
end
Accept
of application/vnd.api+json
?A server MUST support fetching relationship data for every relationship URL
provided as a self link as part of a link object.
The docs provide a sample request for a link related.
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"links": {
"self": "/articles/1/links/author",
"related": "/articles/1/author"
},
"data": {
"type": "people",
"id": "12"
}
}
2 strategies worth considering here:
I tend to think the former is probably better, as the controllers won't really need to be overloaded that much. But one concern is validations.
Validations will come from some model and need to be provided through some kind of controller, but the challenge is making those controller actions for validations work in all cases or making those controllers be a first class citizen (generating a file in /app/controllers/ for links management).
It's possible you could do both, by generating a base links management controller, and then in each sub controller provide an interface for customizing those controllers. Something like:
class ArticleLinksController < Matterhorn::LinksController
def show
# ... something custom
super
end
end
class ArticlesController < Matterorn::Base
manages_links_with ArticlesLinksController
end
Lastly, routing for this will require some extra attention. I'm not really sure what would be best.
See: https://github.com/josevalim/inherited_resources#singletons
Currently the actions generated for controllers are limited to generating
"resources-style" routes. Matterhorn should also support the
addition of resource routing and actions.
We should create a concern, Matterhorn::Resource
and resource!
, to handle
the action generation. The resource method should set the default singleton: true
configuration so the url_helpers generate the correct paths.
Is this a firm requirement as part of Gem or should we make the gem a little smarter. Presently, nested resources throw an exception when Matterhorn::Inclusions::InclusionSupport
is excluded.
1) multi ids resources with_request "GET /#{collection_name}/:id1,:id4/:nested_collection.json" should use multi-ids to scope relationship
Failure/Error: perform_request!
NoMethodError:
undefined method `__inclusion_configs' for #<Mongoid::Criteria:0x007fc328166668>
# ./lib/matterhorn/serialization/scoped.rb:68:in `merge_inclusions!'
# ./lib/matterhorn/serialization/scoped.rb:43:in `block in serializable_hash'
# ./lib/matterhorn/serialization/scoped.rb:42:in `tap'
# ./lib/matterhorn/serialization/scoped.rb:42:in `serializable_hash'
# ./lib/matterhorn/serialization/scoped_collection_serializer.rb:9:in `serializable_hash'
# ./lib/matterhorn/serialization/scoped.rb:48:in `as_json'
# ./lib/matterhorn/responder.rb:12:in `default_render'
# ./lib/matterhorn/resources.rb:111:in `block in index'
# ./lib/matterhorn/resources.rb:213:in `with_scope'
# ./lib/matterhorn/resources.rb:110:in `index'
# ./spec/resources_api/multi_id_resources_spec.rb:83:in `block (3 levels) in <top (required)>'
There are a lot of methods and some that likely are redundant. However, willy-nilly changing breaks gem.
So how do we actually want to manage strong parameters, with our own DSL, using permitted _params
from Inherited Resources, using vanilla permit
?
All responses on all actions are 200
We should extract the basic steps to setup an app using Gem and using the basic matchers to test implementing app.
Example:
gem matterhorn
rails-api new my_json_api_app
class PostsController < Matterhorn::Base
include Matterhorn::Resources
And so forth
If FK is not in items
which is JSON, then lookup can fail.
See the jsonapi paging spec first. Here's what I'm thinking:
class Article
include Mongoid::Document
include Mongoid::Timestamps
end
class Pagination
def initialize(resource, request_env, page_params, options)
end
def next_page_url ; end
def prev_page_url ; end
def first_page_url ; end
# ...
end
class ArticlesController < ApplicationController
include Matterhorn::Ordering
include Matterhorn::Paging
paginates_with Pagination
pagination_params :page
default_order :oldest
end
The above controller should add links as such:
GET /articles.json
{
"links": {
"self" : "http://example.org/articles?page=2",
"next" : "http://example.org/articles?page=3",
"prev" : "http://example.org/articles?page=1",
"first" : "http://example.org/articles",
},
...
The codebase at this time has an InclusionSet object which is used to handle the construction of both the global links header, and additionally the included objects.
To better support serialization, we should create a LinkSet which should house a collection of link objects. There should be a few different types of link objects initially:
Each of these will have different rules for how they are built into links, so each set member will probably have a serializer class for each. Let's talk about an example.
Let's assume you have a a list of articles where each has an author and each user has a vote. Votes are provided as a singleton resource, available as a nested singleton object under each article. Also you want to provide a few comments in the API, let's say the first 3. Let's call these 'initial_comments'
.
So, there are 3 different associations available:
note: the vote is a has_one from the API perspective, but internally it will most likely go through a has_many :votes association in the model.
Here's how the json should look:
GET /articles/1.json
{
"links": {
"self": "http://example.org/articles/1",
"initial_comments" : "http://example.org/articles/{article.initial_comment_ids}",
"vote" : "http://example.org/articles/{article.id}/vote",
"author" : "http://example.org/people/{article.author_id}",
},
"article" : {
"type": "articles",
"id": "1",
"author_id" : "9",
"initial_comment_ids" : ["2, 3"],
"links": {
"self": "http://example.org/articles/1",
"author": {
"related": "http://example.org/people/9",
"linkage": { "type": "people", "id": "9" }
},
"vote": {
"related": "http://example.org/articles/1/vote",
"linkage": { "type": "vote", "article_id": "1" }
},
"initial_comments": {
"related": "http://example.org/comments/2,3",
"linkage": [
{ "type": "comments", "id": "2" },
{ "type": "comments", "id": "3" }
]
}
}
}
}
Requirements:
This is blocking: pagination and inclusion request action handling
Add self, next, prev, first links to our current pagination interface,
if there is no next page, next should be null, same for prev.
GET /articles.json
{
"links": {
"self" : "http://example.org/articles?page=2",
"next" : "http://example.org/articles?page=3",
"prev" : "http://example.org/articles?page=1",
"first" : "http://example.org/articles",
},
"data" : "..."
}
For endpoints that support ordering, links should be added for the different named orders. For example:
GET /articles.json
{
"orders": {
"oldest" : "http://example.org/articles?order=oldest",
"recent" : "http://example.org/articles?order=recent",
}
In order to get an end-to-end implementation going and all small kinks worked out.
GET /posts
GET /posts/1
POST /posts
PATCH /posts/1
DELETE /posts/1
#17Given you have:
#./config/routes.rb
resources :articles
resources :tags
#./app/models
class Tag
include Mongoid::Document
end
class Article
include Mongoid::Document
# see http://mongoid.org/en/mongoid/docs/relations.html#has_and_belongs_to_many
# more details. The `inverse_of: nil` should not be required, it's just
# to demonstrate that this association doesn't need to be dual sided to work
# inside of matterhorn.
has_and_belongs_to_many :tags, inverse_of: nil
end
tag = Tag.create
article = Article.create tags: [tag]
Let's assume you have an article:
GET `http://example.com/articles/1`
{
"links": {
"self": "http://example.com/articles/1"
},
"data": {
"type": "articles",
"id": "1",
"links": {
"tags": {
"related" : "http://example.com/articles/1/tags",
"linkage" : [
{
"type" : "tags",
"id" : 1
}
]
}
}
}
}
Belongs to many should require a nested route in order to display a related link.
Empty belongs to many relationships must provide linkage as an empty array "[]".
In order to hook into and raise exceptions, we should create a standard error callback/method when exceptions happen and allow that to be used by exception handler.
The payload could have meta info like: env, method, params,etc
For example, HoneyBadger has a notify method where a custom payload can be sent:
http://docs.honeybadger.io/article/41-manually-sending-exceptions-and-events
Using JSON API Spec as guide from sections http://jsonapi.org/format/#crud-creating to http://jsonapi.org/format/#crud-creating-responses-other
POST /posts
POST /posts
{
"title": "Ember Hamster",
"body" : "blah blah 111",
"author":
{ "id": "9" }
}
}
}
Currently, the request env is propagated to all serializers by default, which allows access to the url builder, request params, paging, etc. While this is helpful, the concern is that the request env contains params and logic that should not be able to modify a models json representation, e.g. prevent the model from being cached efficiently.
Caching on items inside of data or includes should not be modified by the request env. Scoped Collection and Scoped resource serializers... will still need access to those variables to produce the correct links for things like paging, ordering, filters, etc.
See: https://github.com/blasterpal/matterhorn/blob/show_spec/spec/resources_api/show_spec.rb#L23
hbeaver~/Dropbox/blasterpal/matterhorn (show_spec)$ be rspec spec/resources_api/show_spec.rb:24
/Users/hbeaver/.rbenv/versions/2.1.0/lib/ruby/gems/2.1.0/gems/rspec-core-3.2.2/lib/rspec/core/example_group.rb:634:in `method_missing': undefined local variable or method `resource_id' for RSpec::ExampleGroups::Show:Class (NameError)
from /Users/hbeaver/Dropbox/blasterpal/matterhorn/spec/resources_api/show_spec.rb:14:in `block in <top (required)>'
from /Users/hbeaver/.rbenv/versions/2.1.0/lib/ruby/gems/2.1.0/gems/rspec-core-3.2.2/lib/rspec/core/example_group.rb:363:in `module_exec'
from /Users/hbeaver/.rbenv/versions/2.1.0/lib/ruby/gems/2.1.0/gems/rspec-core-3.2.2/lib/rspec/core/example_group.rb:363:in `subclass'
from /Users/hbeaver/.rbenv/versions/2.1.0/lib/ruby/gems/2.1.0/gems/rspec-core-3.2.2/lib/rspec/core/example_group.rb:253:in `block in define_example_group_method'
from /Users/hbeaver/.rbenv/versions/2.1.0/lib/ruby/gems/2.1.0/gems/rspec-core-3.2.2/lib/rspec/core/dsl.rb:43:in `block in expose_example_group_alias'
from /Users/hbeaver/Dropbox/blasterpal/matterhorn/spec/resources_api/show_spec.rb:3:in `<top (required)>'
from /Users/hbeaver/.rbenv/versions/2.1.0/lib/ruby/gems/2.1.0/gems/rspec-core-3.2.2/lib/rspec/core/configuration.rb:1226:in `load'
from /Users/hbeaver/.rbenv/versions/2.1.0/lib/ruby/gems/2.1.0/gems/rspec-core-3.2.2/lib/rspec/core/configuration.rb:1226:in `block in load_spec_files'
from /Users/hbeaver/.rbenv/versions/2.1.0/lib/ruby/gems/2.1.0/gems/rspec-core-3.2.2/lib/rspec/core/configuration.rb:1224:in `each'
from /Users/hbeaver/.rbenv/versions/2.1.0/lib/ruby/gems/2.1.0/gems/rspec-core-3.2.2/lib/rspec/core/configuration.rb:1224:in `load_spec_files'
from /Users/hbeaver/.rbenv/versions/2.1.0/lib/ruby/gems/2.1.0/gems/rspec-core-3.2.2/lib/rspec/core/runner.rb:97:in `setup'
from /Users/hbeaver/.rbenv/versions/2.1.0/lib/ruby/gems/2.1.0/gems/rspec-core-3.2.2/lib/rspec/core/runner.rb:85:in `run'
from /Users/hbeaver/.rbenv/versions/2.1.0/lib/ruby/gems/2.1.0/gems/rspec-core-3.2.2/lib/rspec/core/runner.rb:70:in `run'
from /Users/hbeaver/.rbenv/versions/2.1.0/lib/ruby/gems/2.1.0/gems/rspec-core-3.2.2/lib/rspec/core/runner.rb:38:in `invoke'
from /Users/hbeaver/.rbenv/versions/2.1.0/lib/ruby/gems/2.1.0/gems/rspec-core-3.2.2/exe/rspec:4:in `<top (required)>'
from /Users/hbeaver/.rbenv/versions/2.1.0/bin/rspec:23:in `load'
from /Users/hbeaver/.rbenv/versions/2.1.0/bin/rspec:23:in `<main>'
Using JSON Spec as guide with Post model that possesses belongs_to
& has_many
:
Starting at: http://jsonapi.org/format/#fetching
Ending and NOT including http://jsonapi.org/format/#fetching-sparse-fieldsets
/posts
/posts/1
While it might make sense to define some of this logic here, this would be great to use for the initial implementation. Currently, the API constructor doesn't allow for nested params.
We should start by adding a method that sets up most of this functionality on our module action methods.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.