rabl-rails 0.5.5 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +2 -2
- data/CHANGELOG.md +8 -0
- data/README.md +106 -82
- data/lib/rabl-rails/compiler.rb +2 -1
- data/lib/rabl-rails/configuration.rb +3 -3
- data/lib/rabl-rails/handler.rb +3 -3
- data/lib/rabl-rails/library.rb +1 -1
- data/lib/rabl-rails/version.rb +1 -1
- data/lib/rabl-rails/visitors/to_hash.rb +1 -1
- data/rabl-rails.gemspec +1 -3
- data/test/helper.rb +2 -2
- data/test/test_compiler.rb +16 -1
- metadata +3 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4fc57874aa491be7a0c8cc778a9d4e0a307740cf4648c3733d5e49f855eb1cc4
|
4
|
+
data.tar.gz: d4024078d1b780d66a297fb0a2aef0a7e37e6c4db392fa577afa1f16686c595c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6b80cd63b8e17d97833f5696d04e82535c154d69b830ea291c7db344c485dead0ffb355cb75a83b1d155b992801f67815a142aa6da2657a4f7ce65820dd7d929
|
7
|
+
data.tar.gz: 05f604364890ce38ab76e311d76c51ce328b2f5bcf1c389b2837f95f17b89c31b7529dcadd42e52d745d11ce1be2e74f8a5a83d2a13e3e9478185a69c5e47d23
|
data/.travis.yml
CHANGED
@@ -4,13 +4,13 @@ dist: trusty
|
|
4
4
|
env:
|
5
5
|
- "RAILS_VERSION=4.2.6"
|
6
6
|
- "RAILS_VERSION=5.2.0"
|
7
|
+
- "RAILS_VERSION=6.1.0"
|
7
8
|
rvm:
|
8
9
|
- 2.5.3
|
9
10
|
- 2.6.0
|
11
|
+
- 2.7.2
|
10
12
|
- jruby
|
11
13
|
before_install:
|
12
14
|
- gem update bundler
|
13
15
|
matrix:
|
14
16
|
fast_finish: true
|
15
|
-
allow_failures:
|
16
|
-
- env: RAILS_VERSION=master
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
# RABL for Rails [![Build Status](https://travis-ci.org/ccocchi/rabl-rails.
|
1
|
+
# RABL for Rails [![Build Status](https://travis-ci.org/ccocchi/rabl-rails.svg?branch=master)](https://travis-ci.org/ccocchi/rabl-rails)
|
2
2
|
|
3
|
-
|
3
|
+
`rabl-rails` is a ruby templating system for rendering your objects in different format (JSON, XML, PLIST).
|
4
4
|
|
5
|
-
|
5
|
+
This gem aims for speed and little memory footprint while letting you build complex response with a very intuitive DSL.
|
6
6
|
|
7
|
-
rabl-rails
|
7
|
+
`rabl-rails` targets **Rails 4.2/5/6 application** and have been testing with MRI and jRuby.
|
8
8
|
|
9
9
|
## Installation
|
10
10
|
|
@@ -17,23 +17,18 @@ gem install rabl-rails
|
|
17
17
|
or add directly to your `Gemfile`
|
18
18
|
|
19
19
|
```
|
20
|
-
gem 'rabl-rails'
|
20
|
+
gem 'rabl-rails', '~> 0.6.0'
|
21
21
|
```
|
22
22
|
|
23
|
-
And that's it !
|
24
|
-
|
25
23
|
## Overview
|
26
24
|
|
27
|
-
|
28
|
-
assuming you have a `Post` model filled with blog posts, and a `PostController` that look like this
|
25
|
+
The gem enables you to build responses using views like you would using HTML/erb/haml.
|
26
|
+
As example, assuming you have a `Post` model filled with blog posts, and a `PostController` that look like this:
|
29
27
|
|
30
28
|
```ruby
|
31
29
|
class PostController < ApplicationController
|
32
|
-
respond_to :html, :json, :xml
|
33
|
-
|
34
30
|
def index
|
35
31
|
@posts = Post.order('created_at DESC')
|
36
|
-
respond_with(@posts)
|
37
32
|
end
|
38
33
|
end
|
39
34
|
```
|
@@ -43,6 +38,7 @@ You can create the following RABL-rails template to express the API output of `@
|
|
43
38
|
```ruby
|
44
39
|
# app/views/post/index.rabl
|
45
40
|
collection :@posts
|
41
|
+
|
46
42
|
attributes :id, :title, :subject
|
47
43
|
child(:user) { attributes :full_name }
|
48
44
|
node(:read) { |post| post.read_by?(@user) }
|
@@ -58,15 +54,13 @@ This would output the following JSON when visiting `http://localhost:3000/posts.
|
|
58
54
|
}]
|
59
55
|
```
|
60
56
|
|
61
|
-
That's a basic overview but there is a lot more to see such as partials, inheritance or fragment caching.
|
62
|
-
|
63
57
|
## How it works
|
64
58
|
|
65
|
-
|
59
|
+
This gem separates compiling, ie. transforming a RABL-rails template into a Ruby hash, and the actual rendering of the object or collection. This allows to only compile the template once (when template caching is enabled) which is the slow part, and only use hashes during rendering.
|
66
60
|
|
67
|
-
The
|
61
|
+
The drawback of compiling the template outside of any rendering context is that we can't access instance variables like usual. Instead, you'll mostly use symbols representing your variables and the gem will retrieve them when needed.
|
68
62
|
|
69
|
-
|
63
|
+
There are places where the gem allows for "dynamic code" -- code that is evaluated at each rendering, such as within `node` or `condition` blocks.
|
70
64
|
|
71
65
|
```ruby
|
72
66
|
# We reference the @posts varibles that will be used at rendering time
|
@@ -79,7 +73,7 @@ node(:read) { |post| post.read_by?(@user) }
|
|
79
73
|
|
80
74
|
The same rule applies for view helpers such as `current_user`
|
81
75
|
|
82
|
-
After the template is compiled into a hash,
|
76
|
+
After the template is compiled into a hash, `rabl-rails` will use a renderer to create the actual output. Currently, JSON, XML and PList formats are supported.
|
83
77
|
|
84
78
|
## Configuration
|
85
79
|
|
@@ -87,6 +81,7 @@ RablRails works out of the box, with default options and fastest engine availabl
|
|
87
81
|
|
88
82
|
```ruby
|
89
83
|
# config/initializers/rabl_rails.rb
|
84
|
+
|
90
85
|
RablRails.configure do |config|
|
91
86
|
# These are the default
|
92
87
|
# config.cache_templates = true
|
@@ -125,7 +120,7 @@ collection :@posts => :articles
|
|
125
120
|
# => { "articles" : [{...}, {...}] }
|
126
121
|
```
|
127
122
|
|
128
|
-
There are rares cases when the template doesn't map directly to any object. In these cases, you can set data to false
|
123
|
+
There are rares cases when the template doesn't map directly to any object. In these cases, you can set data to false.
|
129
124
|
|
130
125
|
```ruby
|
131
126
|
object false
|
@@ -133,27 +128,15 @@ node(:some_count) { |_| @user.posts.count }
|
|
133
128
|
child(:@user) { attribute :name }
|
134
129
|
```
|
135
130
|
|
136
|
-
If you use
|
131
|
+
If you use gems like *decent_exposure* or *focused_controller*, you can use your variable directly without the leading `@`
|
137
132
|
|
138
133
|
```ruby
|
139
134
|
object :object_exposed
|
140
135
|
```
|
141
136
|
|
142
|
-
You can even skip data declaration at all. If you used `respond_with`, rabl-rails will render the data you passed to it.
|
143
|
-
As there is no name, you can set a root via the `root` macro. This allow you to use your template without caring about variables passed to it.
|
144
|
-
|
145
|
-
```ruby
|
146
|
-
# in controller
|
147
|
-
respond_with(@post)
|
148
|
-
|
149
|
-
# in rabl-rails template
|
150
|
-
root :article
|
151
|
-
attribute :title
|
152
|
-
```
|
153
|
-
|
154
137
|
### Attributes / Methods
|
155
138
|
|
156
|
-
|
139
|
+
Adds a new field to the response object, calling the method on the object being rendered. Methods called this way should return natives types from the format you're using (such as `String`, `integer`, etc for JSON). For more complex objects, see `child` nodes.
|
157
140
|
|
158
141
|
```ruby
|
159
142
|
attributes :id, :title, :to_s
|
@@ -162,64 +145,91 @@ attributes :id, :title, :to_s
|
|
162
145
|
You can aliases these attributes in your response
|
163
146
|
|
164
147
|
```ruby
|
165
|
-
attributes
|
166
|
-
# => { "
|
148
|
+
attributes :my_custom_method, as: :title
|
149
|
+
# => { "title" : <result of my_custom_method> }
|
167
150
|
```
|
168
151
|
|
169
|
-
or show attributes
|
152
|
+
or show attributes based on a condition. The currently rendered object is given to the `proc` condition.
|
153
|
+
|
170
154
|
```ruby
|
171
155
|
attributes :published_at, :anchor, if: ->(post) { post.published? }
|
172
156
|
```
|
173
157
|
|
174
158
|
### Child nodes
|
175
159
|
|
176
|
-
|
160
|
+
Changes the object being rendered for the duration of the block. Depending on if you use `node` or `glue`, the result will be added as a new field or merged respectively.
|
161
|
+
|
162
|
+
Data passed can be a method or a reference to an instance variable.
|
177
163
|
|
178
|
-
For example if you have a `Post` model that belongs to a `User`
|
164
|
+
For example if you have a `Post` model that belongs to a `User` and want to add the user's name to your response.
|
179
165
|
|
180
166
|
```ruby
|
181
167
|
object :@post
|
182
|
-
|
168
|
+
|
169
|
+
child(:user, as: :author) do
|
183
170
|
attributes :name
|
184
171
|
end
|
185
|
-
# => { "post"
|
172
|
+
# => { "post": { "author" : { "name" : "John D." } } }
|
186
173
|
```
|
187
174
|
|
188
|
-
|
175
|
+
If instead of having an `author` node in your response you wanted the name at the root level, you can use `glue`:
|
176
|
+
|
189
177
|
```ruby
|
190
|
-
|
191
|
-
|
178
|
+
object :@post
|
179
|
+
|
180
|
+
glue(:user) do
|
181
|
+
attributes :name, as: :author_name
|
192
182
|
end
|
183
|
+
# => { "post": { "author_name" : "John D." } }
|
193
184
|
```
|
194
185
|
|
195
|
-
|
186
|
+
Arbitrary data source can also be passed:
|
196
187
|
|
197
188
|
```ruby
|
198
|
-
|
199
|
-
|
200
|
-
|
189
|
+
# in your controller
|
190
|
+
# @custom_data = [...]
|
191
|
+
|
192
|
+
# in the view
|
193
|
+
child(:@custom_data) do
|
194
|
+
attributes :id, :name
|
201
195
|
end
|
202
|
-
# => { "
|
196
|
+
# => { "custom_data": [...] }
|
203
197
|
```
|
204
198
|
|
205
|
-
###
|
199
|
+
### Constants
|
206
200
|
|
207
|
-
|
201
|
+
Adds a new field to the response using an immutable value.
|
208
202
|
|
209
203
|
```ruby
|
210
|
-
|
211
|
-
|
212
|
-
# => { "user" : { "full_name" : "John Doe" } }
|
204
|
+
const(:api_version, API::VERSION)
|
205
|
+
const(:locale, 'fr_FR')
|
213
206
|
```
|
214
207
|
|
215
|
-
|
208
|
+
### Lookups
|
209
|
+
|
210
|
+
Adds a new field to the response, using rendered resource's id by default or any method to fetch a value from the given hash variable.
|
216
211
|
|
217
212
|
```ruby
|
218
|
-
|
219
|
-
|
213
|
+
collection :@posts
|
214
|
+
|
215
|
+
lookup(:comments_count, :@comments_count, field: :uuid, cast: false)
|
216
|
+
# => [{ "comments_count": 3 }, { "comments_count": 6 }]
|
217
|
+
```
|
218
|
+
|
219
|
+
In the example above, for each post it will fetch the value from `@comments_count` using the post's `uuid` as key. When the `cast` value is set to `true` (it is `false` by default), the value will be casted to a boolean using `!!`.
|
220
|
+
|
221
|
+
|
222
|
+
### Custom nodes
|
223
|
+
|
224
|
+
Adds a new field to the response with block's result as value.
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
object :@user
|
228
|
+
node(:full_name) { |u| u.first_name + " " + u.last_name }
|
229
|
+
# => { "user" : { "full_name" : "John Doe" } }
|
220
230
|
```
|
221
231
|
|
222
|
-
You can add condition on your custom nodes
|
232
|
+
You can add condition on your custom nodes. If the condition evaluates to a falsey value, the node will not added to the response at all.
|
223
233
|
|
224
234
|
```ruby
|
225
235
|
node(:email, if: ->(u) { u.valid_email? }) do |u|
|
@@ -227,13 +237,13 @@ node(:email, if: ->(u) { u.valid_email? }) do |u|
|
|
227
237
|
end
|
228
238
|
```
|
229
239
|
|
230
|
-
Nodes are evaluated at
|
240
|
+
Nodes are evaluated at rendering time, so you can use any instance variables or view helpers within them
|
231
241
|
|
232
242
|
```ruby
|
233
243
|
node(:url) { |post| post_url(post) }
|
234
244
|
```
|
235
245
|
|
236
|
-
If
|
246
|
+
If the result of the block is a Hash, it can be directly merge into the response using `merge` instead of `node`
|
237
247
|
|
238
248
|
```ruby
|
239
249
|
object :@user
|
@@ -241,33 +251,42 @@ merge { |u| { name: u.first_name + " " + u.last_name } }
|
|
241
251
|
# => { "user" : { "name" : "John Doe" } }
|
242
252
|
```
|
243
253
|
|
244
|
-
Custom nodes are really usefull to create flexible representations of your resources.
|
245
|
-
|
246
254
|
### Extends & Partials
|
247
255
|
|
248
256
|
Often objects have a basic representation that is shared accross different views and enriched according to it. To avoid code redundancy you can extend your template from any other RABL template.
|
249
257
|
|
250
258
|
```ruby
|
251
|
-
# app/views/
|
259
|
+
# app/views/shared/_user.rabl
|
252
260
|
attributes :id, :name
|
253
261
|
|
254
|
-
# app/views/users/
|
262
|
+
# app/views/users/show.rabl
|
263
|
+
object :@user
|
264
|
+
|
265
|
+
extends('shared/_user')
|
255
266
|
attributes :super_secret_attribute
|
256
267
|
|
257
|
-
|
258
|
-
|
259
|
-
|
268
|
+
#=> { "id": 1, "name": "John", "super_secret_attribute": "Doe" }
|
269
|
+
```
|
270
|
+
|
271
|
+
When used with child node, if they are the only thing added you can instead use the `partial` option directly.
|
272
|
+
|
273
|
+
```ruby
|
274
|
+
child(:user, partial: 'shared/_user')
|
275
|
+
|
276
|
+
# is equivalent to
|
277
|
+
|
278
|
+
child(:user) do
|
279
|
+
extends('shared/_user')
|
280
|
+
end
|
260
281
|
```
|
261
282
|
|
262
|
-
|
283
|
+
Extends can be used dynamically using rendered object and lambdas.
|
263
284
|
|
264
285
|
```ruby
|
265
|
-
|
266
|
-
attribute :title
|
267
|
-
child(:user, partial: 'users/base')
|
286
|
+
extends ->(user) { "shared/_#{user.client_type}_infos" }
|
268
287
|
```
|
269
288
|
|
270
|
-
Partials can also be used inside custom nodes. When using partial this way, you MUST declare the object associated to the partial
|
289
|
+
Partials can also be used inside custom nodes. When using partial this way, you MUST declare the `object` associated to the partial
|
271
290
|
|
272
291
|
```ruby
|
273
292
|
node(:location) do |user|
|
@@ -275,28 +294,33 @@ node(:location) do |user|
|
|
275
294
|
end
|
276
295
|
```
|
277
296
|
|
278
|
-
When used
|
297
|
+
When used this way, partials can take locals variables that can be accessed in the included template.
|
298
|
+
|
279
299
|
```ruby
|
280
|
-
#
|
300
|
+
# _credit_card.rabl
|
281
301
|
node(:credit_card, if: ->(u) { locals[:display_credit_card] }) do |user|
|
282
302
|
user.credit_card_info
|
283
303
|
end
|
284
304
|
|
285
305
|
# user.json.rabl
|
286
|
-
merge { |u| partial('
|
306
|
+
merge { |u| partial('_credit_card', object: u, locals: { display_credit_card: true }) }
|
287
307
|
```
|
288
308
|
|
289
|
-
###
|
309
|
+
### Putting it all together
|
290
310
|
|
291
|
-
|
311
|
+
`rabl-rails` allows you to format your responses easily, from simple objects to hierarchy of 2 or 3 levels.
|
292
312
|
|
293
313
|
```ruby
|
294
314
|
object :@thread
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
315
|
+
|
316
|
+
attribute :caption, as: :title
|
317
|
+
|
318
|
+
child(:@sorted_posts, as: :posts) do
|
319
|
+
attributes :title, :slug
|
320
|
+
|
321
|
+
child :comments do
|
322
|
+
extends 'shared/_comment'
|
323
|
+
lookup(:upvotes, :@upvotes_per_comment)
|
300
324
|
end
|
301
325
|
end
|
302
326
|
```
|
@@ -328,4 +352,4 @@ Want to make another change ? Just fork and contribute, any help is very much ap
|
|
328
352
|
|
329
353
|
## Copyright
|
330
354
|
|
331
|
-
Copyright © 2012-
|
355
|
+
Copyright © 2012-2020 Christopher Cocchi-Perrier. See [MIT-LICENSE](http://github.com/ccocchi/rabl-rails/blob/master/MIT-LICENSE) for details.
|
data/lib/rabl-rails/compiler.rb
CHANGED
@@ -69,7 +69,8 @@ module RablRails
|
|
69
69
|
#
|
70
70
|
def child(name_or_data, options = {})
|
71
71
|
data, name = extract_data_and_name(name_or_data)
|
72
|
-
name = options[:root]
|
72
|
+
name = options[:root] if options.has_key? :root
|
73
|
+
name = options[:as] if options.has_key? :as
|
73
74
|
template = partial_or_block(data, options) { yield }
|
74
75
|
@template.add_node Nodes::Child.new(name, template)
|
75
76
|
end
|
@@ -33,9 +33,9 @@ module RablRails
|
|
33
33
|
def result_flags
|
34
34
|
@result_flags ||= begin
|
35
35
|
result = 0
|
36
|
-
result |=
|
37
|
-
result |=
|
38
|
-
result |= 0b100
|
36
|
+
result |= 0b001 if @replace_nil_values_with_empty_strings
|
37
|
+
result |= 0b010 if @replace_empty_string_values_with_nil
|
38
|
+
result |= 0b100 if @exclude_nil_values
|
39
39
|
result
|
40
40
|
end
|
41
41
|
end
|
data/lib/rabl-rails/handler.rb
CHANGED
@@ -3,12 +3,12 @@ require 'active_support/core_ext/class/attribute'
|
|
3
3
|
module RablRails
|
4
4
|
module Handlers
|
5
5
|
class Rabl
|
6
|
-
def self.call(template)
|
6
|
+
def self.call(template, source = nil)
|
7
7
|
%{
|
8
8
|
RablRails::Library.instance.
|
9
|
-
get_rendered_template(#{template.source.inspect}, self, local_assigns)
|
9
|
+
get_rendered_template(#{(source || template.source).inspect}, self, local_assigns)
|
10
10
|
}
|
11
11
|
end
|
12
12
|
end
|
13
13
|
end
|
14
|
-
end
|
14
|
+
end
|
data/lib/rabl-rails/library.rb
CHANGED
@@ -25,7 +25,7 @@ module RablRails
|
|
25
25
|
|
26
26
|
def get_rendered_template(source, view, locals = nil)
|
27
27
|
compiled_template = compile_template_from_source(source, view)
|
28
|
-
format = view.lookup_context.
|
28
|
+
format = view.lookup_context.formats.first || :json
|
29
29
|
raise UnknownFormat, "#{format} is not supported in rabl-rails" unless RENDERER_MAP.key?(format)
|
30
30
|
RENDERER_MAP[format].render(compiled_template, view, locals)
|
31
31
|
end
|
data/lib/rabl-rails/version.rb
CHANGED
data/rabl-rails.gemspec
CHANGED
@@ -14,9 +14,7 @@ Gem::Specification.new do |s|
|
|
14
14
|
|
15
15
|
s.required_ruby_version = '>= 2.2.0'
|
16
16
|
|
17
|
-
s.files
|
18
|
-
file.start_with?('test/')
|
19
|
-
end
|
17
|
+
s.files = `git ls-files`.split("\n")
|
20
18
|
s.test_files = `git ls-files -- test/*`.split("\n")
|
21
19
|
s.require_paths = ["lib"]
|
22
20
|
|
data/test/helper.rb
CHANGED
data/test/test_compiler.rb
CHANGED
@@ -11,13 +11,20 @@ class TestCompiler < Minitest::Test
|
|
11
11
|
}
|
12
12
|
end
|
13
13
|
|
14
|
+
@@view_class = if ActionView::Base.respond_to?(:with_empty_template_cache)
|
15
|
+
# From Rails 6.1
|
16
|
+
ActionView::Base.with_empty_template_cache
|
17
|
+
else
|
18
|
+
ActionView::Base
|
19
|
+
end
|
20
|
+
|
14
21
|
describe 'compiler' do
|
15
22
|
def extract_attributes(nodes)
|
16
23
|
nodes.map(&:hash)
|
17
24
|
end
|
18
25
|
|
19
26
|
before do
|
20
|
-
@view = ActionView::
|
27
|
+
@view = @@view_class.new(ActionView::LookupContext.new(@@tmp_path), {}, nil)
|
21
28
|
@compiler = RablRails::Compiler.new(@view)
|
22
29
|
end
|
23
30
|
|
@@ -156,6 +163,14 @@ class TestCompiler < Minitest::Test
|
|
156
163
|
assert_equal(:user, child_node.data)
|
157
164
|
end
|
158
165
|
|
166
|
+
it "compiles child with root name defined with `as` option" do
|
167
|
+
t = @compiler.compile_source(%{ child(:user, as: :author) do attributes :foo end })
|
168
|
+
child_node = t.nodes.first
|
169
|
+
|
170
|
+
assert_equal(:author, child_node.name)
|
171
|
+
assert_equal(:user, child_node.data)
|
172
|
+
end
|
173
|
+
|
159
174
|
it "compiles child with arbitrary source" do
|
160
175
|
t = @compiler.compile_source(%{ child :@user => :author do attribute :name end })
|
161
176
|
child_node = t.nodes.first
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rabl-rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christopher Cocchi-Perrier
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-03-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -158,8 +158,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
158
158
|
- !ruby/object:Gem::Version
|
159
159
|
version: '0'
|
160
160
|
requirements: []
|
161
|
-
|
162
|
-
rubygems_version: 2.7.6
|
161
|
+
rubygems_version: 3.1.4
|
163
162
|
signing_key:
|
164
163
|
specification_version: 4
|
165
164
|
summary: Fast Rails 4+ templating system with JSON, XML and PList support
|