simple_ams 0.1.0

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 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