simple_ams 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5193c4f1997cfc84c8fdf5fa90abfde7ac2a03a2
4
+ data.tar.gz: 13c6c665417899d0a73d9900144d58e8f848e370
5
+ SHA512:
6
+ metadata.gz: b6affb011516c6e9b69b829c176966305b38f7d102b874248bac63fb089c8b84b27cec6190e067364943935b26164bcc066d12ebad77c88d412edaf7db6724aa
7
+ data.tar.gz: c44a84ff86b1a0d2eb5b67670b508a3cfa6da2ded1cc0353fd0709b73a352a7a2d1bf735860cba76ae03f696e4eefab50c4704b67229be8fb1285f43376c8ea2
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /coverage/
11
+ .envrc
12
+ # rspec failure tracking
13
+ .rspec_status
14
+ .todo.md
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.1
5
+ before_install: gem install bundler -v 1.15.4
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in SimpleAMS.gemspec
6
+ gemspec
data/README.md ADDED
@@ -0,0 +1,224 @@
1
+ # SimpleAMS
2
+ > "Simple things should be simple and complex things should be possible." Alan Kay.
3
+
4
+ If we want to interact with modern APIs we should start building modern, flexible libraries
5
+ that help developers to build such APIs. Modern Ruby serializers, as I always wanted them to be.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'simple_ams'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install simple_ams
22
+
23
+ ## Usage
24
+ The gem's interface has been inspired by ActiveModel Serializers 0.9.2, 0.10.stable, jsonapi-rb and Ember Data.
25
+ However, **it has been built for POROs, does not rely in any dependency and does not relate to Rails in any case** other than
26
+ some nostalgia for the (advanced at that time) pre-0.10 ActiveModel Serialiers.
27
+
28
+
29
+ ### Simple case
30
+
31
+ You will rarely need all the advanced options. Usually you will have something like that:
32
+
33
+ ```ruby
34
+ class UserSerializer
35
+ include SimpleAMS::DSL
36
+
37
+ #specify the adapter, pass some options all the way down to the adapter
38
+ adapter SimpleAMS::Adapters::JSONAPI, root: true
39
+
40
+ #specify available attributes/fields
41
+ attributes :id, :name, :email, :birth_date
42
+
43
+ #specify available relations
44
+ has_many :videos, :comments, :posts
45
+ belongs_to :organization
46
+ has_one :profile
47
+
48
+ #specify some links
49
+ link :feed, '/api/v1/me/feed'
50
+ #links can also take other options, as specified by RFC 8288
51
+ link :root, '/api/v1/', rel: :user
52
+ #link values can be dynamic as well through lambdas
53
+ link :posts, ->(obj) { "/api/v1/users/#{obj.id}/posts/" }, rel: :user
54
+ #if you also need dynamic options, you can return an array from the lambda
55
+ link :followers, ->(obj) { ["/api/v1/users/#{obj.id}/followers/", rel: obj.type] }
56
+
57
+ #same with metas: can be static, dynamic and accept arbitrary options
58
+ meta :environment, ->(obj) { Rails.env.to_s }
59
+
60
+ #collection accepts exactly the same aforementioned interface
61
+ #although you will rarely use it to full extend
62
+ #here we use only links and meta
63
+ collection do
64
+ link :root, '/api/v1/', rel: :user
65
+ type :users
66
+ meta :count, ->(collection) { collection.count }
67
+ end
68
+
69
+ #note that there is a shortcut if you just need to specify the collection name/type:
70
+ #collection :users
71
+
72
+ #override an attribute
73
+ def name
74
+ "#{object.first_name} #{object.last_name}"
75
+ end
76
+
77
+ #override a relation
78
+ def videos
79
+ Videos.where(user_id: object.id).published
80
+ end
81
+ end
82
+ ```
83
+
84
+ Then you can just feed your serializer with data, along with some options:
85
+
86
+ ```ruby
87
+ SimpleAMS::Renderer.new(user, fields: [:id, :name, :email], includes: [:videos]).to_json
88
+ ```
89
+ `to_json` first calls `as_json`, which creates a ruby Hash and then `to_json` is called
90
+ on top of that hash.
91
+
92
+
93
+ # Advanced usage
94
+ The DSL in the previous example is just syntactic sugar. In the basis, there is a very powerful
95
+ hash-based DSL that can be used in 3 different places:
96
+
97
+ * When initializing the `SimpleAMS::Renderer` class to render the data using specific serializer, adapter and options.
98
+ * Inside a class that has the `SimpleAMS::DSL` included, using the `with_options({})` class method
99
+ * Through the DSL, powered with some syntactic sugar
100
+
101
+ In any case, we have the following options:
102
+
103
+ ```ruby
104
+ {
105
+ #the primary id of the record(s), used mostly by the underlying adapter (like JSONAPI)
106
+ primary_id: :id,
107
+ #the type of the record, used mostly by the underlying adapter (like JSONAPI)
108
+ type: :user,
109
+ #which relations should be included
110
+ includes: [:posts, videos: [:comments]],
111
+ #which fields for each relation should be included
112
+ fields: [:id, :name, posts: [:id, :text], videos: [:id, :title, comments: [:id, :text]]] #overrides includes when association is specified
113
+ relations: [
114
+ [:belongs_to, :company, {
115
+ serializer: CompanySerializer,
116
+ fields: Company.column_names.map(&:to_sym)
117
+ }
118
+ ],
119
+ [:has_many, :followers, {
120
+ serializer: UserSerializer,
121
+ fields: User.column_names.map(&:to_sym)
122
+ ],
123
+ ]
124
+ #the serializer that should be used
125
+ #makes sense to use it when initializing the Renderer
126
+ serializer: UserSerializer,
127
+ #can also be a lambda, in case of polymorphic records, ideal for ArrayRenderer
128
+ serializer: ->(obj){ obj.employee? ? EmployeeSerializer : UserSerializer }
129
+ #specifying the underlying adapter. This cannot be a lambda in case of ArrayRenderer,
130
+ #but can take some useful options that are passed down straight to the adapter class.
131
+ adapter: SimpleAMS::Adapters::AMS, root: true
132
+ #the links data
133
+ links: {
134
+ #can be a simple string
135
+ root: '/api/v1'
136
+ #a string with some options (relation and target attributes as defined by RFC8288
137
+ #however, you can also pass adapter-specific attributes
138
+ posts: "/api/v1/posts/", rel: :posts,
139
+ #it can also be a lambda that takes the resource to be rendered as a param
140
+ #when the lambda is called, it should return the array structure above
141
+ self: ->(obj) { ["/api/v1/users/#{obj.id}", rel: :user] }
142
+ },
143
+ #the meta data, same as the links data (available in adapters even for single records)
144
+ metas: {
145
+ type: ->(obj){ obj.employee? ? :employee : :user}
146
+ #meta can take arbitrary options as well
147
+ authorization: :oauth, type: :bearer_token
148
+ },
149
+ #collection parameters, used only in ArrayRenderer
150
+ collection: {
151
+ links: {
152
+ root: '/api/v1'
153
+ },
154
+ metas: {
155
+ pages: ->(obj) { [obj.pages, collection: true]},
156
+ current_page: ->(obj) { [obj.current_page, collection: true] },
157
+ previous_page: ->(obj) { [obj.previous_page, collection: true] },
158
+ next_page: ->(obj) { [obj.next_page, collection: true] },
159
+ max_per_page: 50,
160
+ },
161
+ }
162
+ #exposing helpers that will be available inside the seriralizer
163
+ expose: {
164
+ #a class
165
+ current_user: User.first
166
+ #or a module
167
+ helpers: CommonHelpers
168
+ },
169
+ }
170
+ ```
171
+
172
+ Now let those options be `OPTIONS`. These can be fed to either the `SimpleAMS::Renderer`
173
+ or to the serializer class itself using the `with_options` class method. Let's see how:
174
+
175
+ ```ruby
176
+ class UserSerializer
177
+ include SimpleAMS::DSL
178
+
179
+ with_options({ #you can pass the same options as above ;)
180
+ primary_id: :id,
181
+ # ...
182
+ # ...
183
+ # ...
184
+ })
185
+
186
+ def name
187
+ "#{object.first_name} #{object.last_name}"
188
+ end
189
+
190
+ def videos
191
+ Videos.where(user_id: object.id).published
192
+ end
193
+ end
194
+ ```
195
+
196
+ The same options can be passed when calling the `Renderer`. `Renderer` can override
197
+ some properties, however in all properties that act as sets/arrays (like
198
+ attributes/fields, includes, links etc.), **specified serializer options take precedence** over
199
+ `Renderer` options.
200
+
201
+ ```ruby
202
+ SimpleAMS::Renderer.new(user, {
203
+ primary_id: :id,
204
+ serializer: UserSerializer,
205
+ # ...
206
+ # ...
207
+ # ...
208
+ }).to_json
209
+
210
+ ```
211
+ ## Development
212
+
213
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
214
+
215
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
216
+
217
+ ## Contributing
218
+ But reports are very welcome at https://github.com/vasilakisfil/SimpleAMS. Please add as much info as you can (serializer and Renderer input)
219
+ so that we can easily track down the bug.
220
+
221
+ Pull requests are also very welcome on GitHub at https://github.com/vasilakisfil/SimpleAMS.
222
+ However, to keep the code's sanity (AMS I am looking to you), **I will be very picky** on the code style and design,
223
+ to match (my) existing code characteristics.
224
+ Because at the end of the day, it's gonna be me who will maintain this thing.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "SimpleAMS"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,85 @@
1
+ require "simple_ams"
2
+
3
+ class SimpleAMS::Adapters::AMS
4
+ attr_reader :document, :options
5
+
6
+ def initialize(document, options = {})
7
+ @document = document
8
+ @options = options
9
+ end
10
+
11
+ def as_json
12
+ hash = {}
13
+
14
+ #TODO: I think bang method for merging is way faster ?
15
+ hash = hash.merge(fields)
16
+ hash = hash.merge(relations)
17
+ hash = hash.merge(links: links) unless links.empty?
18
+ hash = hash.merge(metas: metas) unless metas.empty?
19
+
20
+ return hash
21
+ end
22
+
23
+ def fields
24
+ @fields ||= document.fields.inject({}){ |hash, field|
25
+ _value = field.value
26
+ hash[field.key] = _value.respond_to?(:as_json) ? _value.as_json : _value
27
+ hash
28
+ }
29
+ end
30
+
31
+ def links
32
+ @links ||= document.links.inject({}){ |hash, link|
33
+ _value = link.value
34
+ hash[link.name] = _value.respond_to?(:as_json) ? _value.as_json : _value
35
+ hash
36
+ }
37
+ end
38
+
39
+ def metas
40
+ @metas ||= document.metas.inject({}){ |hash, meta|
41
+ _value = meta.value
42
+ hash[meta.name] = _value.respond_to?(:as_json) ? _value.as_json : _value
43
+ hash
44
+ }
45
+ end
46
+
47
+ def relations
48
+ return {} if document.relations.empty?
49
+
50
+ @relations ||= document.relations.inject({}){ |hash, relation|
51
+ if relation.folder?
52
+ value = relation.documents.map{|doc| self.class.new(doc).as_json}
53
+ else
54
+ value = self.class.new(relation).as_json
55
+ end
56
+ hash[relation.name] = value
57
+
58
+ hash
59
+ }
60
+ end
61
+
62
+ class Collection < self
63
+ attr_reader :folder, :adapter, :options
64
+
65
+ def initialize(folder, options = {})
66
+ @folder = folder
67
+ @adapter = folder.adapter.value
68
+ @options = options
69
+ end
70
+
71
+ def as_json
72
+ if options[:root]
73
+ {folder.name => documents}
74
+ else
75
+ documents
76
+ end
77
+ end
78
+
79
+ def documents
80
+ return folder.documents.map{|document|
81
+ adapter.new(document).as_json
82
+ } || []
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,2 @@
1
+ module SimpleAMS::Adapters
2
+ end
@@ -0,0 +1,66 @@
1
+ require "simple_ams"
2
+
3
+ module SimpleAMS
4
+ class Document::Fields
5
+ include Enumerable
6
+
7
+ attr_reader :members
8
+
9
+ def initialize(options)
10
+ @options = options
11
+ @members = options.fields #[:field1, :field2]
12
+ end
13
+
14
+ def [](key)
15
+ found = members.find{|field| field == key}
16
+ return nil unless found
17
+
18
+ return with_decorator(found)
19
+ end
20
+
21
+ def each(&block)
22
+ return enum_for(:each) unless block_given?
23
+
24
+ members.each{ |key|
25
+ yield with_decorator(key)
26
+ }
27
+
28
+ self
29
+ end
30
+
31
+ private
32
+ attr_reader :options
33
+
34
+ def with_decorator(key)
35
+ Field.new(
36
+ options.resource,
37
+ options.serializer,
38
+ key,
39
+ options
40
+ )
41
+ end
42
+
43
+ class Field
44
+ attr_reader :key
45
+
46
+ #do we need to inject the whole options object?
47
+ def initialize(resource, serializer, key, options)
48
+ @resource = resource
49
+ @serializer = serializer
50
+ @key = key
51
+ @options = options
52
+ end
53
+
54
+ def value
55
+ return @value if defined?(@value)
56
+
57
+ return @value = serializer.send(key) if serializer.respond_to? key
58
+ binding.pry if resource.is_a?(Array)
59
+ return resource.send(key)
60
+ end
61
+
62
+ private
63
+ attr_reader :resource, :serializer
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,61 @@
1
+ require "simple_ams"
2
+
3
+ module SimpleAMS
4
+ class Document::Links
5
+ include Enumerable
6
+
7
+ attr_reader :members
8
+
9
+ def initialize(options)
10
+ @options = options
11
+ @members = options.links
12
+ end
13
+
14
+ def [](key)
15
+ found = members.find{|link| link.name == key}
16
+ return nil unless found
17
+
18
+ return with_decorator(found)
19
+ end
20
+
21
+ def each(&block)
22
+ return enum_for(:each) unless block_given?
23
+
24
+ members.each{ |member|
25
+ yield with_decorator(member)
26
+ }
27
+
28
+ self
29
+ end
30
+
31
+ private
32
+ attr_reader :options
33
+
34
+ def with_decorator(link)
35
+ Link.new(link)
36
+ end
37
+
38
+ #memoization maybe ?
39
+ class Link
40
+ def initialize(link)
41
+ @link = link
42
+ end
43
+
44
+ def name
45
+ link.name
46
+ end
47
+
48
+ def value
49
+ link.respond_to?(:call) ? link.value.call : link.value
50
+ end
51
+
52
+ def options
53
+ link.options
54
+ end
55
+
56
+ private
57
+ attr_reader :link
58
+ end
59
+ end
60
+ end
61
+
@@ -0,0 +1,15 @@
1
+ require "simple_ams"
2
+
3
+ module SimpleAMS
4
+ class Document::Metas < Document::Links
5
+ def initialize(options)
6
+ super
7
+ @members = options.metas
8
+ end
9
+
10
+ class Meta < Document::Links::Link
11
+ end
12
+ end
13
+ end
14
+
15
+
@@ -0,0 +1,95 @@
1
+ require "simple_ams"
2
+
3
+ #TODO: Add memoization for the relations object (iteration + access)
4
+ module SimpleAMS
5
+ class Document::Relations
6
+ include Enumerable
7
+
8
+ def initialize(options)
9
+ @options = options
10
+ @relations = options.relations
11
+ @serializer = options.serializer
12
+ @resource = options.resource
13
+ end
14
+
15
+ def [](key)
16
+ found = relations.find{|relation| relation.name == key}
17
+ return nil unless found
18
+
19
+ return relation_for(found)
20
+ end
21
+
22
+ def each(&block)
23
+ return enum_for(:each) unless block_given?
24
+
25
+ relations.each{ |relation|
26
+ yield relation_for(relation)
27
+ }
28
+
29
+ self
30
+ end
31
+
32
+ def empty?
33
+ count == 0
34
+ end
35
+
36
+ private
37
+ attr_reader :options, :relations, :serializer, :resource
38
+
39
+ def relation_for(relation)
40
+ renderer_klass_for(relation).new(
41
+ SimpleAMS::Options.new(
42
+ relation_value_for(relation.name), relation_options_for(relation)
43
+ )
44
+ )
45
+ end
46
+
47
+ #TODO: rename that to relation and existing relation to relationship
48
+ def relation_value_for(name)
49
+ if serializer.respond_to?(name)
50
+ serializer.send(name)
51
+ else
52
+ resource.send(name)
53
+ end
54
+ end
55
+
56
+ #4 options are merged:
57
+ # *user injected when instantiating the SimpleAMS class
58
+ # *relation options injected from parent serializer
59
+ # *serializer class options
60
+ def relation_options_for(relation)
61
+ _relation_options = {
62
+ injected_options: (relation.options || {}).merge(
63
+ options.relation_options_for(
64
+ relation.name
65
+ ).merge(
66
+ expose: options.expose
67
+ )
68
+ ).merge(
69
+ _internal: {
70
+ module: serializer.class.to_s.rpartition('::').first
71
+ }
72
+ )
73
+ }
74
+ #TODO: deep merge, can we automate this somehow ?
75
+ _relation_options[:injected_options][:collection] = (_relation_options[:collection] || {}).merge(
76
+ name: relation.name
77
+ )
78
+
79
+ return _relation_options
80
+ end
81
+
82
+ def renderer_klass_for(relation)
83
+ renderer = SimpleAMS::Document
84
+ collection_renderer = renderer::Folder
85
+
86
+ relation.collection? ? collection_renderer : renderer
87
+ end
88
+
89
+ =begin TODO: Add that as public method, should help performance in edge cases
90
+ def relationship_info_for(name)
91
+ relations.find{|i| i.name == name}
92
+ end
93
+ =end
94
+ end
95
+ end