roaster 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +5 -0
- data/Gemfile.lock +67 -0
- data/Rakefile +5 -0
- data/STUFF +105 -0
- data/lib/roaster.rb +12 -0
- data/lib/roaster/adapters/active_record.rb +116 -0
- data/lib/roaster/decorator.rb +65 -0
- data/lib/roaster/json_api.rb +13 -0
- data/lib/roaster/query.rb +144 -0
- data/lib/roaster/request.rb +73 -0
- data/lib/roaster/resource.rb +41 -0
- data/lib/roaster/version.rb +3 -0
- data/roaster.gemspec +30 -0
- data/test/activerecord_adapter_test.rb +124 -0
- data/test/decorator_test.rb +10 -0
- data/test/factories/album.rb +25 -0
- data/test/factories/band.rb +9 -0
- data/test/factories/track.rb +5 -0
- data/test/mappings/album.rb +19 -0
- data/test/mappings/band.rb +6 -0
- data/test/mappings/track.rb +8 -0
- data/test/models/album.rb +14 -0
- data/test/models/band.rb +11 -0
- data/test/models/namespaced/model.rb +4 -0
- data/test/models/track.rb +13 -0
- data/test/query_target_test.rb +29 -0
- data/test/query_test.rb +103 -0
- data/test/request_test.rb +16 -0
- data/test/resource_test.rb +11 -0
- data/test/support/active_record.rb +11 -0
- data/test/test_helper.rb +27 -0
- data/test/wut_test.rb +205 -0
- metadata +183 -4
@@ -0,0 +1,73 @@
|
|
1
|
+
module Roaster
|
2
|
+
class Request
|
3
|
+
|
4
|
+
ALLOWED_OPERATIONS = [:create, :read, :update, :delete]
|
5
|
+
|
6
|
+
#TODO: Move this elsewhere (factory)
|
7
|
+
def self.mapping_class_from_target(target)
|
8
|
+
if target.relationship_name
|
9
|
+
mapping_class_from_name(target.relationship_name)
|
10
|
+
else
|
11
|
+
mapping_class_from_name(target.resource_name)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.mapping_class_from_name(name)
|
16
|
+
"#{name.to_s.singularize}_mapping".classify.constantize
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(operation, target, resource, params, opts = {})
|
20
|
+
# :create, :read, :update, :delete
|
21
|
+
unless ALLOWED_OPERATIONS.include?(operation)
|
22
|
+
raise "#{operation} is not a valid operation."
|
23
|
+
end
|
24
|
+
@operation = operation
|
25
|
+
@resource = resource
|
26
|
+
@mapping_class = opts[:mapping_class] || self.class.mapping_class_from_target(target)
|
27
|
+
@query = Roaster::Query.new(@operation, target, @mapping_class, params)
|
28
|
+
#TODO: Oh snap this is confusing
|
29
|
+
@document = opts[:document]
|
30
|
+
end
|
31
|
+
|
32
|
+
def execute
|
33
|
+
case @operation
|
34
|
+
when :create
|
35
|
+
#TODO: - IDEA - Maybe make `new` return a fake 'relationship' object so a relationship special case wouldn't be needed
|
36
|
+
if @query.target.relationship_name.nil?
|
37
|
+
obj = @resource.new(@query)
|
38
|
+
parse(obj, @document)
|
39
|
+
@resource.save(obj)
|
40
|
+
else
|
41
|
+
@resource.create_relationship(@query, @document)
|
42
|
+
end
|
43
|
+
when :read
|
44
|
+
res = @resource.query(@query)
|
45
|
+
represent(res).to_hash
|
46
|
+
when :update
|
47
|
+
obj = @resource.find(@query)
|
48
|
+
links = @document.delete('links')
|
49
|
+
@resource.update_relationships(@query, links) if links
|
50
|
+
parse(obj, @document)
|
51
|
+
@resource.save(obj)
|
52
|
+
when :delete
|
53
|
+
@resource.delete(@query)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def parse(object, data)
|
60
|
+
rp = @mapping_class.new(object)
|
61
|
+
rp.from_hash(data)
|
62
|
+
end
|
63
|
+
|
64
|
+
def represent(data)
|
65
|
+
if data.respond_to?(:each)
|
66
|
+
@mapping_class.for_collection.prepare(data)
|
67
|
+
else
|
68
|
+
@mapping_class.prepare(data)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Roaster
|
2
|
+
|
3
|
+
class Resource
|
4
|
+
|
5
|
+
def initialize(adapter_class, opts = {})
|
6
|
+
@adapter = adapter_class.new
|
7
|
+
@model_class = opts[:model_class]
|
8
|
+
end
|
9
|
+
|
10
|
+
def new(query)
|
11
|
+
@adapter.new(query)
|
12
|
+
end
|
13
|
+
|
14
|
+
def save(model)
|
15
|
+
@adapter.save(model)
|
16
|
+
model
|
17
|
+
end
|
18
|
+
|
19
|
+
def delete(query)
|
20
|
+
@adapter.delete(query)
|
21
|
+
end
|
22
|
+
|
23
|
+
def create_relationship(query, document)
|
24
|
+
@adapter.create_relationship(query, document)
|
25
|
+
end
|
26
|
+
|
27
|
+
def update_relationships(query, document)
|
28
|
+
@adapter.update_relationship(query, document)
|
29
|
+
end
|
30
|
+
|
31
|
+
def find(query)
|
32
|
+
@adapter.find(query, model_class: @model_class)
|
33
|
+
end
|
34
|
+
|
35
|
+
def query(query)
|
36
|
+
@adapter.read(query, model_class: @model_class)
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
data/roaster.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/roaster/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.name = 'roaster'
|
6
|
+
gem.authors = ['Nicolas Albeza', 'Jérémy Lecerf']
|
7
|
+
gem.email = ['n.albeza@gmail.com', 'redpist.com@gmail.com']
|
8
|
+
gem.description = %q{Model/JSONAPI mapping}
|
9
|
+
gem.summary = %q{Expose your models through a JSONAPI API with a simple mapping}
|
10
|
+
gem.homepage = 'https://github.com/pause/roaster'
|
11
|
+
gem.license = 'MIT'
|
12
|
+
|
13
|
+
gem.files = `git ls-files`.split($\)
|
14
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
15
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
16
|
+
gem.require_paths = ['lib']
|
17
|
+
gem.version = Roaster::VERSION
|
18
|
+
|
19
|
+
gem.add_runtime_dependency "representable", "~> 2.0"
|
20
|
+
gem.add_runtime_dependency "activerecord", "~> 4.1"
|
21
|
+
gem.add_runtime_dependency "activesupport", "~> 4.1"
|
22
|
+
|
23
|
+
gem.add_development_dependency 'minitest', '~> 5.1'
|
24
|
+
gem.add_development_dependency 'rake', '~> 10.3'
|
25
|
+
gem.add_development_dependency 'factory_girl', '~> 4.4'
|
26
|
+
gem.add_development_dependency 'sqlite3', '~> 1.3'
|
27
|
+
gem.add_development_dependency 'database_cleaner', '~> 1.3'
|
28
|
+
gem.add_development_dependency 'byebug', '~> 3.4'
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
|
3
|
+
require 'roaster/query'
|
4
|
+
require 'roaster/adapters/active_record'
|
5
|
+
|
6
|
+
require_relative 'test_helper'
|
7
|
+
require_relative 'models/album'
|
8
|
+
#require_relative 'models/namespaced/model'
|
9
|
+
|
10
|
+
class ActiveRecordAdapterTest < MiniTest::Test
|
11
|
+
|
12
|
+
def setup
|
13
|
+
super
|
14
|
+
@adapter = Roaster::Adapters::ActiveRecord.new
|
15
|
+
@album_mapping = AlbumMapping
|
16
|
+
@albums_target = Roaster::Query::Target.new(:albums)
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_interface
|
20
|
+
assert @adapter.respond_to?(:new)
|
21
|
+
assert @adapter.respond_to?(:save)
|
22
|
+
assert @adapter.respond_to?(:find)
|
23
|
+
assert @adapter.respond_to?(:read)
|
24
|
+
assert @adapter.respond_to?(:delete)
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_mapping_class_from_resource_name
|
28
|
+
mc = Roaster::Adapters::ActiveRecord.model_class_from_resource_name(:albums)
|
29
|
+
assert_equal Album, mc
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_simple_model_for
|
33
|
+
model = @adapter.send(:model_for, :albums)
|
34
|
+
assert_equal model, Album
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_custom_model_for
|
38
|
+
model = @adapter.send(:model_for, Album)
|
39
|
+
assert_equal model, Album
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_new
|
43
|
+
query = build_query(:create)
|
44
|
+
model_instance = @adapter.new(query)
|
45
|
+
assert_kind_of Album, model_instance
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_save
|
49
|
+
title = 'Serial Smokers'
|
50
|
+
query = build_query(:create)
|
51
|
+
model_instance = @adapter.new(query)
|
52
|
+
model_instance.send(:title=, title)
|
53
|
+
@adapter.save(model_instance)
|
54
|
+
|
55
|
+
new_album = Album.last
|
56
|
+
assert_equal model_instance, new_album
|
57
|
+
assert_equal title, new_album.title
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_find
|
61
|
+
ref_album = FactoryGirl.create(:album)
|
62
|
+
target = Roaster::Query::Target.new(:albums, ref_album.id)
|
63
|
+
#TODO: API problems (why do i need a full query here ?!)!
|
64
|
+
query = build_query(:update, target)
|
65
|
+
album = @adapter.find(query)
|
66
|
+
|
67
|
+
assert_equal album, ref_album
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def call_adapter_method(method,
|
73
|
+
target,
|
74
|
+
params = {})
|
75
|
+
query = Roaster::Query.new(method, target, @mapping, params)
|
76
|
+
@adapter.send(method, query)
|
77
|
+
end
|
78
|
+
|
79
|
+
def build_query(operation, target = @albums_target, mapping = @album_mapping, params = {})
|
80
|
+
Roaster::Query.new(:create, @albums_target, @album_mapping, params)
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_new
|
84
|
+
query = build_query(:create)
|
85
|
+
model_instance = @adapter.new(query)
|
86
|
+
assert_kind_of Album, model_instance
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_save
|
90
|
+
title = 'Serial Smokers'
|
91
|
+
query = build_query(:create)
|
92
|
+
model_instance = @adapter.new(query)
|
93
|
+
model_instance.send(:title=, title)
|
94
|
+
@adapter.save(model_instance)
|
95
|
+
|
96
|
+
new_album = Album.last
|
97
|
+
assert_equal model_instance, new_album
|
98
|
+
assert_equal title, new_album.title
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_find
|
102
|
+
ref_album = FactoryGirl.create(:album)
|
103
|
+
target = Roaster::Query::Target.new(:albums, ref_album.id)
|
104
|
+
#TODO: API problems (why do i need a full query here ?!)!
|
105
|
+
query = build_query(:update, target)
|
106
|
+
album = @adapter.find(query)
|
107
|
+
|
108
|
+
assert_equal album, ref_album
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def call_adapter_method(method,
|
114
|
+
target,
|
115
|
+
params = {})
|
116
|
+
query = Roaster::Query.new(method, target, @mapping, params)
|
117
|
+
@adapter.send(method, query)
|
118
|
+
end
|
119
|
+
|
120
|
+
def build_query(operation, target = @albums_target, mapping = @album_mapping, params = {})
|
121
|
+
Roaster::Query.new(:create, @albums_target, @album_mapping, params)
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative './band'
|
2
|
+
|
3
|
+
FactoryGirl.define do
|
4
|
+
factory :album do
|
5
|
+
title 'Album Title'
|
6
|
+
band
|
7
|
+
|
8
|
+
factory :pink_floyd_albums do
|
9
|
+
association :band, factory: :pink_floyd
|
10
|
+
|
11
|
+
factory :animals_album do
|
12
|
+
title 'Animals'
|
13
|
+
end
|
14
|
+
|
15
|
+
factory :the_wall_album do
|
16
|
+
title 'The Wall'
|
17
|
+
end
|
18
|
+
|
19
|
+
factory :meddle_album do
|
20
|
+
title 'Meddle'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'roaster/json_api'
|
2
|
+
|
3
|
+
class AlbumMapping < Roaster::JsonApi::Mapping
|
4
|
+
|
5
|
+
property :title
|
6
|
+
#property :created_at
|
7
|
+
|
8
|
+
# TODO: auto include included mapping
|
9
|
+
# Aiming: Nested sparse fieldsets authorizations and default behaviour for sort
|
10
|
+
can_include :band, :tracks
|
11
|
+
|
12
|
+
can_filter_by :band, :title
|
13
|
+
|
14
|
+
can_sort_by :band, :title, :created_at, band: [:name]
|
15
|
+
|
16
|
+
# resource_name 'test_lautre'
|
17
|
+
|
18
|
+
collection_representer class: Album
|
19
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
require 'active_record'
|
3
|
+
|
4
|
+
ActiveRecord::Migration.class_eval do
|
5
|
+
create_table :albums do |t|
|
6
|
+
t.string :title
|
7
|
+
t.belongs_to :band
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Album < ActiveRecord::Base
|
12
|
+
belongs_to :band
|
13
|
+
has_many :tracks
|
14
|
+
end
|
data/test/models/band.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
|
3
|
+
require 'roaster/query'
|
4
|
+
|
5
|
+
require_relative 'test_helper'
|
6
|
+
|
7
|
+
class QueryTargetTest < MiniTest::Test
|
8
|
+
|
9
|
+
def setup
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_defaults
|
14
|
+
target = Roaster::Query::Target.new(:albums)
|
15
|
+
assert_equal target.resource_name, :albums
|
16
|
+
assert_equal target.resource_ids, []
|
17
|
+
assert_equal target.relationship_name, nil
|
18
|
+
assert_equal target.relationship_ids, []
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_init
|
22
|
+
target = Roaster::Query::Target.new(:albums, 1, :tracks, [2, 3])
|
23
|
+
assert_equal target.resource_name, :albums
|
24
|
+
assert_equal target.resource_ids, [1]
|
25
|
+
assert_equal target.relationship_name, :tracks
|
26
|
+
assert_equal target.relationship_ids, [2, 3]
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|