welo 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- data/README +122 -0
- data/Rakefile +45 -0
- data/TODO +8 -0
- data/lib/welo.rb +8 -0
- data/lib/welo/base/resource.rb +69 -0
- data/lib/welo/core/link.rb +123 -0
- data/lib/welo/core/matcher.rb +101 -0
- data/lib/welo/core/nesting.rb +19 -0
- data/lib/welo/core/perspective.rb +12 -0
- data/lib/welo/core/relationship.rb +44 -0
- data/lib/welo/core/resource.rb +319 -0
- metadata +74 -0
data/README
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
|
2
|
+
= Welo
|
3
|
+
|
4
|
+
Welo is a library to work with resources in a general framework.
|
5
|
+
It deals with the following concerns:
|
6
|
+
- relationships between resources
|
7
|
+
- structuring and access control with perspectives: one may or may not be able to access all fields of a resource
|
8
|
+
- identifiers and epithets
|
9
|
+
- serialization of resources
|
10
|
+
|
11
|
+
== Perspectives
|
12
|
+
|
13
|
+
When you look at a house, you usually can see walls, windows, a door, and a
|
14
|
+
roof. If you can enter the house, then you will see rooms, furniture,
|
15
|
+
inhabitants and so on. Depending on the room where you are, you will not see
|
16
|
+
the roof. The concept of perspective is basically that: depending on where you
|
17
|
+
look from (or who you are), you cannot observe the same parts of the same
|
18
|
+
object. Perspectives have a name, and a set of symbols which corresponds to
|
19
|
+
what someone can observe through this perspective.
|
20
|
+
|
21
|
+
A piece of code for the above example could be:
|
22
|
+
|
23
|
+
class House
|
24
|
+
include Welo::Resource
|
25
|
+
perspective :outside, [:walls, :windows, :roof, :door]
|
26
|
+
perspective :inside, [:rooms, :furniture, :inhabitants]
|
27
|
+
end
|
28
|
+
|
29
|
+
== Identifiers
|
30
|
+
|
31
|
+
Say you have a lots of shirts, and picked one for a party. A close friend calls
|
32
|
+
you and ask you which clothes you're taking. Then, you'll describe your shirt
|
33
|
+
like "the black shirt with vertical stripes". You've just identified your
|
34
|
+
shirt, by, giving enough information to uniquelly identify a shirt. Then,
|
35
|
+
depending on the situation, you may identify a shirt differently. As an
|
36
|
+
example, the shop where you've bought your shirt has a reference number, and
|
37
|
+
maybe a barcode to identify it. By default, Welo will add a 'uuid' method
|
38
|
+
(which is aliased to the object_id) for identifying a resource. You can always
|
39
|
+
override that.
|
40
|
+
|
41
|
+
class Shirt
|
42
|
+
include Welo::Resource
|
43
|
+
# identify :default, :uuid #what Welo does for you
|
44
|
+
identify :friend, :colors, :stripes, :length
|
45
|
+
identify :shop, :refnum
|
46
|
+
end
|
47
|
+
|
48
|
+
== Relationships
|
49
|
+
|
50
|
+
Often, two things are related whith each other. With respect to the state of
|
51
|
+
the art. So far, a person may have many shirts but has only two biological
|
52
|
+
parents: a father and a mother. Currently, Welo has relationships of :one and
|
53
|
+
:many kinds of relationships. This allow you to define some elaborate structures:
|
54
|
+
|
55
|
+
class Person
|
56
|
+
include Welo::Resource
|
57
|
+
relationship :shirts, :Shirt, :many
|
58
|
+
relationship :father, :Person, :one
|
59
|
+
relationship :mother, :Person, :one
|
60
|
+
end
|
61
|
+
|
62
|
+
Someone used to relational system may notice that we only wrote one part of the
|
63
|
+
relationships: if a person has another Person as a parent, therefore, the
|
64
|
+
parents should have a sort of symmetrical relationship. For simplicity, Welo does not force you to write this symmetry relationship if you will not be using it in your program.
|
65
|
+
Similarly, for now, there are no complex associations, like through
|
66
|
+
associations. Welo's goal is not to build an ORM which optimizes SQL queries.
|
67
|
+
|
68
|
+
Finally, there are two ways to qualify relationships: Nestings and Epithets.
|
69
|
+
These two qualifiers are related to the various ways of identifying related
|
70
|
+
resources.
|
71
|
+
|
72
|
+
=== Nesting
|
73
|
+
Nestings, let you use an external way of identifying a resource. In that case,
|
74
|
+
the resource identification's scheme depends on the resources, but the value of
|
75
|
+
the identification is within the nested resource itself. As an example, for a
|
76
|
+
company, an employee will be identified upon its social security number,
|
77
|
+
whereas, in a pub, friends will use his name.
|
78
|
+
Nesting, just mark a preferred way of identifying related resources.
|
79
|
+
|
80
|
+
class Person
|
81
|
+
include Welo::Resource
|
82
|
+
identify :friend, :name
|
83
|
+
identify :company, :ssn
|
84
|
+
relationship :friends, :Person, :many
|
85
|
+
nesting :friends, :friend
|
86
|
+
end
|
87
|
+
|
88
|
+
class Company
|
89
|
+
include Welo::Resource
|
90
|
+
relationship :employees, :Person, :many
|
91
|
+
nesting :employees, :company
|
92
|
+
end
|
93
|
+
|
94
|
+
=== Epithets
|
95
|
+
Epithets are similar to nestings, but the way of identifying a resource is only
|
96
|
+
related to the pointing resource. As an example, a person may have three
|
97
|
+
preferred movies. This naming scheme is only personal to the person, but not to
|
98
|
+
the movies. So, if you know that an object has preferred movies, you don't need
|
99
|
+
to know anything about the movie to identify it within the context of a person.
|
100
|
+
For now, declaring requires you must first specify that the matching
|
101
|
+
relationship is a mere alias, then declare the epithet's method for identifying
|
102
|
+
the related resource.
|
103
|
+
|
104
|
+
class Movie
|
105
|
+
include Welo::Resource
|
106
|
+
end
|
107
|
+
|
108
|
+
class Person
|
109
|
+
include Welo::Resource
|
110
|
+
relationship :preferred_movies, :Movie, :many, :alias
|
111
|
+
epithet :preferred_movies, :rank_for_preffered_movie
|
112
|
+
|
113
|
+
def rank_for_preffered_movie(movie)
|
114
|
+
@preferred_movies.index(movie)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
== Coming later
|
119
|
+
|
120
|
+
* Embedding (similar to mongo's embedded documents).
|
121
|
+
* ResourceProxys to manipulate distant resources, or resources observed from one perspective, with missing fields.
|
122
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
|
2
|
+
require 'rubygems'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
|
5
|
+
$LOAD_PATH.unshift('lib')
|
6
|
+
require 'welo'
|
7
|
+
|
8
|
+
spec = Gem::Specification.new do |s|
|
9
|
+
s.name = 'welo'
|
10
|
+
s.rubyforge_project = 'welo'
|
11
|
+
s.version = Welo::VERSION
|
12
|
+
s.author = Welo::AUTHORS.first
|
13
|
+
s.homepage = Welo::WEBSITE
|
14
|
+
s.summary = "A light resource model for Ruby"
|
15
|
+
s.email = "crapooze@gmail.com"
|
16
|
+
s.platform = Gem::Platform::RUBY
|
17
|
+
|
18
|
+
s.files = [
|
19
|
+
'Rakefile',
|
20
|
+
'TODO',
|
21
|
+
'README',
|
22
|
+
'lib/welo.rb',
|
23
|
+
'lib/welo/core/resource.rb',
|
24
|
+
'lib/welo/core/relationship.rb',
|
25
|
+
'lib/welo/core/link.rb',
|
26
|
+
'lib/welo/core/matcher.rb',
|
27
|
+
'lib/welo/core/nesting.rb',
|
28
|
+
'lib/welo/core/perspective.rb',
|
29
|
+
'lib/welo/base/resource.rb',
|
30
|
+
]
|
31
|
+
|
32
|
+
s.require_path = 'lib'
|
33
|
+
s.bindir = 'bin'
|
34
|
+
s.executables = []
|
35
|
+
s.has_rdoc = true
|
36
|
+
end
|
37
|
+
|
38
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
39
|
+
pkg.need_tar = true
|
40
|
+
end
|
41
|
+
|
42
|
+
task :gem => ["pkg/#{spec.name}-#{spec.version}.gem"] do
|
43
|
+
puts "generated #{spec.version}"
|
44
|
+
end
|
45
|
+
|
data/TODO
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
* Relationships
|
2
|
+
- we have :one, :many, we should be able to have, exactly_four, or one_per_foo
|
3
|
+
* Epitheting vs. Nesting vs. Identifying
|
4
|
+
- remove alias from Relationship and create a similar Epiteth object?
|
5
|
+
* Embeddings
|
6
|
+
- recursively embedded datastructures?
|
7
|
+
- how to work with perspectives?
|
8
|
+
* ResourceProxy
|
data/lib/welo.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
|
2
|
+
require 'welo/core/resource'
|
3
|
+
module Welo
|
4
|
+
module Resource
|
5
|
+
|
6
|
+
# returns a text representation of the resource, under
|
7
|
+
# the persp perspective,
|
8
|
+
# the format is not standardized, suitable for dev cycles
|
9
|
+
def to_text(persp)
|
10
|
+
ret = ''
|
11
|
+
structure_pairs(persp).each do |k,val|
|
12
|
+
ret << "##{k}\n"
|
13
|
+
if val.respond_to? :map
|
14
|
+
ret << val.map{|v| v.to_s}.join("\n")
|
15
|
+
else
|
16
|
+
ret << val.to_s
|
17
|
+
end
|
18
|
+
ret << "\n"
|
19
|
+
end
|
20
|
+
ret
|
21
|
+
end
|
22
|
+
|
23
|
+
# returns an array of pairs of fields name and fields values
|
24
|
+
# for the perspective.
|
25
|
+
#
|
26
|
+
# the order of the pairs is the same as the fields in the perspective
|
27
|
+
#
|
28
|
+
# Links to other resources are represented as urls by sending
|
29
|
+
# them the :to_s method.
|
30
|
+
# the name of this method comes from the fact that Links are serialized to
|
31
|
+
# strings
|
32
|
+
def serialized_pairs(persp)
|
33
|
+
ary = structure_pairs(persp).map do |sym, val|
|
34
|
+
i1 = case val
|
35
|
+
when Link
|
36
|
+
val.to_s
|
37
|
+
when LinksEnumerator
|
38
|
+
val.map{|v| v.to_s}
|
39
|
+
else
|
40
|
+
val
|
41
|
+
end
|
42
|
+
[sym, i1]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# similar to to_rb_hash, but with the urls serialized
|
47
|
+
def to_serialized_hash(persp)
|
48
|
+
Hash[serialized_pairs(persp)]
|
49
|
+
end
|
50
|
+
|
51
|
+
# returns a JSON representation of the resource, under
|
52
|
+
# the persp perspective
|
53
|
+
def to_json(persp)
|
54
|
+
to_serialized_hash(persp).to_json
|
55
|
+
end
|
56
|
+
|
57
|
+
# returns a YAML representation of the resource, under
|
58
|
+
# the persp perspective
|
59
|
+
def to_YAML(persp)
|
60
|
+
YAML.dump to_serialized_hash(persp)
|
61
|
+
end
|
62
|
+
|
63
|
+
# shortcut to call to_#{ext}
|
64
|
+
def to_ext(ext, persp=:default)
|
65
|
+
meth = ext.sub(/^\./, 'to_')
|
66
|
+
send(meth, persp)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
|
2
|
+
module Welo
|
3
|
+
module CanBeLazy
|
4
|
+
# A flag to say if this object is lazy or not
|
5
|
+
attr_accessor :lazy
|
6
|
+
|
7
|
+
# A hash of blocks between methods name and lazy evaluations
|
8
|
+
attr_accessor :lazy_blocks
|
9
|
+
|
10
|
+
def lazy_blocks
|
11
|
+
@lazy_blocks ||= {}
|
12
|
+
end
|
13
|
+
|
14
|
+
# true if lazy
|
15
|
+
def lazy?
|
16
|
+
(@lazy && true) or @lazy.nil?
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Link
|
21
|
+
include CanBeLazy
|
22
|
+
|
23
|
+
# The pointing resource
|
24
|
+
attr_accessor :from
|
25
|
+
|
26
|
+
# The pointed resource
|
27
|
+
attr_accessor :to
|
28
|
+
|
29
|
+
# The labeling for this link
|
30
|
+
attr_accessor :label
|
31
|
+
|
32
|
+
# The locality for this link
|
33
|
+
attr_accessor :local
|
34
|
+
|
35
|
+
# Creates a link to the pointed resource to,
|
36
|
+
# it must respond to :path
|
37
|
+
def initialize(from, to=nil, params={}, &blk)
|
38
|
+
@from = from
|
39
|
+
@to = to
|
40
|
+
@lazy = params.has_key?(:lazy) ? params[:lazy] : true
|
41
|
+
@label = params[:label]
|
42
|
+
@local = params[:local]
|
43
|
+
lazy_blocks[:to] = blk if block_given?
|
44
|
+
end
|
45
|
+
|
46
|
+
# If the link is lazy and there is no value to @to,
|
47
|
+
# will evaluate the :to lazy_block and set to to this value.
|
48
|
+
# If the link is not lazy, will just return @to
|
49
|
+
def to
|
50
|
+
if @lazy
|
51
|
+
@to ||= lazy_blocks[:to].call
|
52
|
+
else
|
53
|
+
@to
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Gets the nesting of the from resource of this link.
|
58
|
+
def nesting
|
59
|
+
@nesting ||= from.nesting(label)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Gets the identifying scheme corresponding to this link.
|
63
|
+
# For now the identification scheme is written in the to resource.
|
64
|
+
# Later, we'll try to be able to identify pointed resources with
|
65
|
+
# an identifying scheme local to the from resource.
|
66
|
+
def identify
|
67
|
+
identify = if nesting
|
68
|
+
nesting.identifier_sym
|
69
|
+
else
|
70
|
+
:default
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns an absolute version of the path.
|
75
|
+
# Meaning that we use the default identifying scheme.
|
76
|
+
def to_absolute_path
|
77
|
+
File.join('', to.path(:default).to_s)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns a relative version of the path.
|
81
|
+
# We can use a different identification scheme of the pointed resource.
|
82
|
+
# Later, we'll try to be able to identify pointed resources with
|
83
|
+
# a identifying scheme local to the from resource.
|
84
|
+
def to_relative_path
|
85
|
+
File.join('.', to.path(identify).to_s)
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_local_path
|
89
|
+
File.join('.', from.epithet_path_to(to, label).to_s)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns a string representation of this link
|
93
|
+
# (i.e., an path)
|
94
|
+
def to_s
|
95
|
+
if nesting
|
96
|
+
to_relative_path
|
97
|
+
elsif local
|
98
|
+
to_local_path
|
99
|
+
else
|
100
|
+
to_absolute_path
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class LinksEnumerator
|
106
|
+
include Enumerable
|
107
|
+
|
108
|
+
# the enumerating object
|
109
|
+
attr_reader :enum
|
110
|
+
|
111
|
+
def initialize(enum=nil, params={}, &blk)
|
112
|
+
@enum = enum || blk
|
113
|
+
end
|
114
|
+
|
115
|
+
def each
|
116
|
+
if enum.respond_to? :call
|
117
|
+
enum.call{|i| yield(i)}
|
118
|
+
else
|
119
|
+
enum.each{|i| yield(i)}
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
|
2
|
+
module Welo
|
3
|
+
class Matcher
|
4
|
+
attr_reader :params, :prefix
|
5
|
+
|
6
|
+
# Initializes a new matcher for the set of params (a hash) a prefix may be
|
7
|
+
# given, in which case, only the keys prefixed by the prefix
|
8
|
+
# will be selected.
|
9
|
+
def initialize(params, prefix='')
|
10
|
+
@prefix = prefix.dup.freeze
|
11
|
+
@params = if prefix.empty?
|
12
|
+
params.dup.freeze
|
13
|
+
else
|
14
|
+
h = {}
|
15
|
+
keys = params.keys.select do |k|
|
16
|
+
k.start_with?(prefix)
|
17
|
+
end
|
18
|
+
keys.each do |k|
|
19
|
+
h[k] = params[k]
|
20
|
+
end
|
21
|
+
h.freeze
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def missing_params?(*args)
|
26
|
+
missing_params(*args).any?
|
27
|
+
end
|
28
|
+
|
29
|
+
def too_many_params?(*args)
|
30
|
+
extra_params(*args).any?
|
31
|
+
end
|
32
|
+
|
33
|
+
def wrong_params_set?(*args)
|
34
|
+
missing_params?(*args) or
|
35
|
+
too_many_params?(*args)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
class IdentifyingMatcher < Matcher
|
41
|
+
def identifiers_for_params_matching(resource, ident=:default)
|
42
|
+
resource.identifiers(ident, prefix).map{|id| id.sub(/^:/,'')}
|
43
|
+
end
|
44
|
+
|
45
|
+
def missing_params(resource, ident=:default)
|
46
|
+
(identifiers_for_params_matching(resource, ident) - params.keys)
|
47
|
+
end
|
48
|
+
|
49
|
+
def extra_params(resource, ident=:default)
|
50
|
+
(params.keys - identifiers_for_params_matching(resource, ident))
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns true if the params hash qualifies for identifying this resource.
|
54
|
+
# prefixing is possible in case the hash's keys has prefixes
|
55
|
+
# e.g., Foo = Struct.new(:a, :b) do
|
56
|
+
# include Resource
|
57
|
+
# identify :default, :a, :b
|
58
|
+
# end
|
59
|
+
# foo = Foo.new(1,2)
|
60
|
+
# params = { 'bla.a' => 1, 'bla.b' => 2 }
|
61
|
+
# Matcher.new(params, 'bla.').match?(foo, :default)
|
62
|
+
# # => true
|
63
|
+
# see Resource#identifiers and identifiers_for_params_matching for more on prefix.
|
64
|
+
def match?(resource, ident=:default)
|
65
|
+
return false if wrong_params_set?(resource, ident)
|
66
|
+
pairs = [resource.identify(ident), identifiers_for_params_matching(resource, ident)].transpose
|
67
|
+
pairs.inject(true) do |bool, pair|
|
68
|
+
sym, param_name = *pair
|
69
|
+
bool and resource.send(sym) == params[param_name]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
alias :=~ :match?
|
74
|
+
end
|
75
|
+
|
76
|
+
class EpithetMatcher < Matcher
|
77
|
+
def epithets_for_params_matching(resource, epithet_resource, label)
|
78
|
+
resource.epithets(label, prefix).map{|id| id.sub(/^:/,'')}
|
79
|
+
end
|
80
|
+
|
81
|
+
def missing_params(resource, epithet_resource, label)
|
82
|
+
(epithets_for_params_matching(resource, epithet_resource, label) - params.keys)
|
83
|
+
end
|
84
|
+
|
85
|
+
def extra_params(resource, epithet_resource, label)
|
86
|
+
(params.keys - epithets_for_params_matching(resource, epithet_resource, label))
|
87
|
+
end
|
88
|
+
|
89
|
+
def match?(resource, epithet_resource, label)
|
90
|
+
return false if wrong_params_set?(resource, epithet_resource, label)
|
91
|
+
pairs = [resource.epithet(label),
|
92
|
+
epithets_for_params_matching(resource, epithet_resource, label)].transpose
|
93
|
+
pairs.inject(true) do |bool, pair|
|
94
|
+
sym, param_name = *pair
|
95
|
+
bool and resource.send(sym, epithet_resource) == params[param_name]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
alias :=~ :match?
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
|
2
|
+
module Welo
|
3
|
+
# Nesting are not embeddings they just points to other resources with local
|
4
|
+
# identification namings scheme
|
5
|
+
class Nesting
|
6
|
+
# The symbol for the method to call to get the
|
7
|
+
# nested resource
|
8
|
+
attr_accessor :resource_sym
|
9
|
+
|
10
|
+
# The symbol for the method to identify the
|
11
|
+
# nested resource
|
12
|
+
attr_accessor :identifier_sym
|
13
|
+
|
14
|
+
def initialize(sym1, sym2)
|
15
|
+
@resource_sym = sym1
|
16
|
+
@identifier_sym = sym2
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
|
2
|
+
module Welo
|
3
|
+
class Relationship
|
4
|
+
# The symbol for the method to call to get the
|
5
|
+
# related resource
|
6
|
+
attr_accessor :sym
|
7
|
+
|
8
|
+
# The kinds of relationship
|
9
|
+
attr_accessor :kinds
|
10
|
+
|
11
|
+
# The klass of the related resource
|
12
|
+
attr_accessor :klass
|
13
|
+
|
14
|
+
# Returns true if the kinds are incompatible
|
15
|
+
# current incompatibilities:
|
16
|
+
# - :one and :many at the same time
|
17
|
+
def self.incompatible_kinds?(kinds)
|
18
|
+
(kinds & [:one, :many]).size == 2
|
19
|
+
end
|
20
|
+
|
21
|
+
# creates a new relationship for sym and klass, with all the given kinds
|
22
|
+
# providing incompatibles kinds lead to an ArgumentError
|
23
|
+
def initialize(sym, klass, kinds)
|
24
|
+
raise ArgumentError, "incompatible kinds" if self.class.incompatible_kinds?(kinds)
|
25
|
+
@sym = sym
|
26
|
+
@klass = klass
|
27
|
+
@kinds = kinds
|
28
|
+
end
|
29
|
+
|
30
|
+
# true if at least one of the kinds is :one
|
31
|
+
def one?
|
32
|
+
kinds.include?(:one)
|
33
|
+
end
|
34
|
+
|
35
|
+
# true if at least one of the kinds is :many
|
36
|
+
def many?
|
37
|
+
kinds.include?(:many)
|
38
|
+
end
|
39
|
+
|
40
|
+
def alias?
|
41
|
+
kinds.include?(:alias)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,319 @@
|
|
1
|
+
|
2
|
+
module Welo
|
3
|
+
autoload :Link, 'welo/core/link'
|
4
|
+
autoload :LinksEnumerator, 'welo/core/link'
|
5
|
+
autoload :Relationship, 'welo/core/relationship'
|
6
|
+
autoload :Perspective, 'welo/core/perspective'
|
7
|
+
autoload :Nesting, 'welo/core/nesting'
|
8
|
+
autoload :IdentifyingMatcher, 'welo/core/matcher'
|
9
|
+
autoload :EpithetMatcher, 'welo/core/matcher'
|
10
|
+
|
11
|
+
module Resource
|
12
|
+
|
13
|
+
# A simple hook to extend the ClassMethods
|
14
|
+
def self.included(mod)
|
15
|
+
mod.extend ClassMethods
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
# A hook to duplicates
|
20
|
+
# * relationships
|
21
|
+
# * perspectives
|
22
|
+
# * identification
|
23
|
+
# * base_path
|
24
|
+
# Such that the subclass looks like the parent one
|
25
|
+
def inherited(klass)
|
26
|
+
relationships.each_pair do |k,v|
|
27
|
+
klass.relationships[k] = v
|
28
|
+
end
|
29
|
+
nestings.each_pair do |k,v|
|
30
|
+
klass.nestings[k] = v
|
31
|
+
end
|
32
|
+
perspectives.each_pair do |k,v|
|
33
|
+
klass.perspectives[k] = v
|
34
|
+
end
|
35
|
+
identifiers_hash.each_pair do |k,v|
|
36
|
+
klass.identifiers_hash[k] = v
|
37
|
+
end
|
38
|
+
klass.base_path base_path
|
39
|
+
end
|
40
|
+
|
41
|
+
# The hash of relationships with other resources
|
42
|
+
def relationships
|
43
|
+
@relationships ||= {}
|
44
|
+
end
|
45
|
+
|
46
|
+
# The hash of nestings of other resources
|
47
|
+
def nestings
|
48
|
+
@nestings ||= {}
|
49
|
+
end
|
50
|
+
|
51
|
+
# If one argument, returns the relationship with the given name
|
52
|
+
# If more than one argument, registers a new relationship,
|
53
|
+
# possibly overwriting it.
|
54
|
+
def relationship(sym, klass=nil, kinds=[])
|
55
|
+
if klass
|
56
|
+
relationships[sym] = Relationship.new(sym, klass, kinds)
|
57
|
+
else
|
58
|
+
relationships[sym]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# The hash of perspectives for this resource.
|
63
|
+
def perspectives
|
64
|
+
@perspectives ||= {}
|
65
|
+
end
|
66
|
+
|
67
|
+
# If one argument, returns the perspective for the given name,
|
68
|
+
# or the :default one if it exists
|
69
|
+
# If more than one argument, registers a new perspective
|
70
|
+
def perspective(name, syms=nil)
|
71
|
+
if syms.nil?
|
72
|
+
perspectives[name] || perspectives[:default]
|
73
|
+
else
|
74
|
+
perspectives[name] = Perspective.new(name, syms)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# If one argument, returns the nesting for the given resource_sym
|
79
|
+
# If two arguments: creates a nesting of a resource in this resource
|
80
|
+
def nesting(resource_sym, identifier_sym=nil)
|
81
|
+
if identifier_sym.nil?
|
82
|
+
nestings[resource_sym]
|
83
|
+
else
|
84
|
+
nestings[resource_sym] = Nesting.new(resource_sym, identifier_sym)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Returns the downcased last part of the ruby name
|
89
|
+
def default_base_path_name
|
90
|
+
self.name.split('::').last.downcase
|
91
|
+
end
|
92
|
+
|
93
|
+
# If there is one argument, sets the base_path,
|
94
|
+
# otherwise returns the base_path, if not set, will fallback to
|
95
|
+
# the default_base_path_name
|
96
|
+
def base_path(val=nil)
|
97
|
+
if val
|
98
|
+
@base_path = val
|
99
|
+
else
|
100
|
+
@base_path || default_base_path_name
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# An hash to log the identifiers, by default, uses [:uuid]
|
105
|
+
def identifiers_hash
|
106
|
+
@identifiers_hash ||= {:default => [:uuid]}
|
107
|
+
end
|
108
|
+
|
109
|
+
# If there is no argument, returns the identification fields
|
110
|
+
# (defaults to [:uuid]).
|
111
|
+
# Otherwise, sets the identification fields.
|
112
|
+
# Fields are symbols of methods that will be sent to the instances.
|
113
|
+
# Since they may be used to build an URL, identifying fields
|
114
|
+
# must respond to :to_s.
|
115
|
+
# See Resource#identifying_path_part
|
116
|
+
def identify(sym, vals=nil)
|
117
|
+
if vals.nil?
|
118
|
+
identifiers_hash[sym]
|
119
|
+
else
|
120
|
+
identifiers_hash[sym] = vals
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Returns array of formatted strings from the identification fields.
|
125
|
+
# a prefix may be appended before the identification field
|
126
|
+
# e.g.
|
127
|
+
# 'foo' (no prefix) => 'foo'
|
128
|
+
# 'foo' (prefix:'bla.') => 'bla.foo'
|
129
|
+
def identifiers(ident, prefix='')
|
130
|
+
identify(ident).map{|sym| "#{prefix}#{sym}"}
|
131
|
+
end
|
132
|
+
|
133
|
+
def epithets_hash
|
134
|
+
@epithets_hash ||= {}
|
135
|
+
end
|
136
|
+
|
137
|
+
def epithet(sym, vals=nil)
|
138
|
+
if vals.nil?
|
139
|
+
epithets_hash[sym]
|
140
|
+
else
|
141
|
+
epithets_hash[sym] = vals
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Returns array of formatted strings from the epithets fields.
|
146
|
+
# a prefix may be appended before the epithets field
|
147
|
+
# e.g.
|
148
|
+
# 'foo' (no prefix) => 'foo'
|
149
|
+
# 'foo' (prefix:'bla.') => 'bla.foo'
|
150
|
+
def epithets(label, prefix='')
|
151
|
+
epithet(label).map{|sym| "#{prefix}#{sym}"}
|
152
|
+
end
|
153
|
+
|
154
|
+
# Returns the path model for items of this resource,
|
155
|
+
# including an optional prefix
|
156
|
+
# e.g. class Foo
|
157
|
+
# base_path 'foo'
|
158
|
+
# identify :default, :bar
|
159
|
+
# end
|
160
|
+
#
|
161
|
+
# Foo.path_model(ident, 'pfx.')
|
162
|
+
# #=> 'foo/:pfx.bar'
|
163
|
+
def path_model(ident, prefix='')
|
164
|
+
File.join(base_path, identifiers(ident, ':' + prefix))
|
165
|
+
end
|
166
|
+
|
167
|
+
# Returns the structure associated to the selected perspective
|
168
|
+
# the structure replace the perspective's fields by relationships whenever
|
169
|
+
# it's possible
|
170
|
+
def structure(name)
|
171
|
+
persp = perspective(name)
|
172
|
+
raise ArgumentError.new("no such perspective: #{name} for #{self}") unless persp
|
173
|
+
fields = persp.fields
|
174
|
+
fields.map do |field|
|
175
|
+
rel = relationship(field)
|
176
|
+
(rel || field)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end # ClassMethods
|
180
|
+
|
181
|
+
# Shorthand for class' base_path
|
182
|
+
def base_path
|
183
|
+
self.class.base_path
|
184
|
+
end
|
185
|
+
|
186
|
+
# Shorthand for class' path_model
|
187
|
+
def path_model(ident=:default, prefix='')
|
188
|
+
self.class.path_model(ident, prefix)
|
189
|
+
end
|
190
|
+
|
191
|
+
# Shorthand for class' identify
|
192
|
+
def identify(ident=:default)
|
193
|
+
self.class.identify(ident)
|
194
|
+
end
|
195
|
+
|
196
|
+
# Shorthand for class' epithet
|
197
|
+
def epithet(name)
|
198
|
+
self.class.epithet(name)
|
199
|
+
end
|
200
|
+
|
201
|
+
# Shorthand for class' perspective
|
202
|
+
def perspective(sym)
|
203
|
+
self.class.perspective(sym)
|
204
|
+
end
|
205
|
+
|
206
|
+
# Shorthand for class' relationship
|
207
|
+
def relationship(sym)
|
208
|
+
self.class.relationship(sym)
|
209
|
+
end
|
210
|
+
|
211
|
+
# Shorthand for class' nesting
|
212
|
+
def nesting(resource_sym)
|
213
|
+
self.class.nesting(resource_sym)
|
214
|
+
end
|
215
|
+
|
216
|
+
# Returns the resource's path part mapping the various fields.
|
217
|
+
# The objects must respond to .to_s and return a value such that
|
218
|
+
# the URL part is valid (esp. no whitespace)
|
219
|
+
# This method does NOT check wether the return of .to_s builds a
|
220
|
+
# valid URL part
|
221
|
+
def identifying_path_part(ident=:default)
|
222
|
+
File.join identify(ident).map{|sym| send(sym).to_s}
|
223
|
+
end
|
224
|
+
|
225
|
+
# Returns the full URL of this object
|
226
|
+
def path(ident=:default)
|
227
|
+
tail = identifying_path_part(ident)
|
228
|
+
if tail.empty?
|
229
|
+
base_path
|
230
|
+
else
|
231
|
+
File.join base_path, tail
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Same as Resource.identifiers
|
236
|
+
def identifiers(ident=:default, prefix='')
|
237
|
+
identify(ident).map{|sym| ":#{prefix}#{sym}"}
|
238
|
+
end
|
239
|
+
|
240
|
+
# Shorthand for class' structure
|
241
|
+
def structure(name)
|
242
|
+
self.class.structure(name)
|
243
|
+
end
|
244
|
+
|
245
|
+
# Returns the structure as an array of pairs which first object is the
|
246
|
+
# field name, the second object being the value.
|
247
|
+
# When the class' structure mandates a Relationship, then the
|
248
|
+
# second object of the pair is a Link to the related resource,
|
249
|
+
# or an LinksEnumerator if it has many resources.
|
250
|
+
def structure_pairs(name)
|
251
|
+
structure(name).map do |rel|
|
252
|
+
sym = nil
|
253
|
+
ret = case rel
|
254
|
+
when Symbol
|
255
|
+
sym = rel
|
256
|
+
send(sym)
|
257
|
+
when Relationship
|
258
|
+
sym = rel.sym
|
259
|
+
link_for_rel(rel)
|
260
|
+
end #case rel
|
261
|
+
[sym, ret]
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
def link_for_rel(rel)
|
266
|
+
if rel.one?
|
267
|
+
single_link_for_rel(rel)
|
268
|
+
elsif rel.many?
|
269
|
+
links_enumerator_for_rel(rel)
|
270
|
+
else
|
271
|
+
raise ArgumentError, "unkown relationship kinds #{rel.kinds}"
|
272
|
+
end #case rel.kind
|
273
|
+
end
|
274
|
+
|
275
|
+
# returns a lazy link loader for the relationship
|
276
|
+
# i.e., the evaluation of the sym method may be delayed
|
277
|
+
def single_link_for_rel(rel)
|
278
|
+
sym = rel.sym
|
279
|
+
Link.new(self, nil, :label => sym, :local => rel.alias?) do
|
280
|
+
send(sym)
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# returns a lazy link loader enumerator for the relationship
|
285
|
+
# it will yield a link
|
286
|
+
def links_enumerator_for_rel(rel)
|
287
|
+
sym = rel.sym
|
288
|
+
LinksEnumerator.new do |&blk|
|
289
|
+
send(sym).each do |i|
|
290
|
+
blk.call Link.new(self, i, :label => sym, :local => rel.alias?)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
def match_params?(params, ident=:default, prefix='')
|
296
|
+
IdentifyingMatcher.new(params, prefix).match?(self, ident)
|
297
|
+
end
|
298
|
+
|
299
|
+
alias uuid object_id
|
300
|
+
|
301
|
+
def epitheting_path_part(resource, label)
|
302
|
+
File.join epithet(label).map{|sym| send(sym, resource).to_s}
|
303
|
+
end
|
304
|
+
|
305
|
+
def epithet_path_to(resource, label)
|
306
|
+
File.join label.to_s, epitheting_path_part(resource,label)
|
307
|
+
end
|
308
|
+
|
309
|
+
# Same as Resource.epithets
|
310
|
+
def epithets(label, prefix='')
|
311
|
+
epithet(label).map{|sym| ":#{prefix}#{sym}"}
|
312
|
+
end
|
313
|
+
|
314
|
+
def epithet_resource_match_params?(resource, params, label, prefix='')
|
315
|
+
EpithetMatcher.new(params, prefix).match?(self, resource, label)
|
316
|
+
end
|
317
|
+
|
318
|
+
end # Resource
|
319
|
+
end # Welo
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: welo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 6
|
9
|
+
version: 0.0.6
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- crapooze
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2011-02-14 00:00:00 +01:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description:
|
22
|
+
email: crapooze@gmail.com
|
23
|
+
executables: []
|
24
|
+
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files: []
|
28
|
+
|
29
|
+
files:
|
30
|
+
- Rakefile
|
31
|
+
- TODO
|
32
|
+
- README
|
33
|
+
- lib/welo.rb
|
34
|
+
- lib/welo/core/resource.rb
|
35
|
+
- lib/welo/core/relationship.rb
|
36
|
+
- lib/welo/core/link.rb
|
37
|
+
- lib/welo/core/matcher.rb
|
38
|
+
- lib/welo/core/nesting.rb
|
39
|
+
- lib/welo/core/perspective.rb
|
40
|
+
- lib/welo/base/resource.rb
|
41
|
+
has_rdoc: true
|
42
|
+
homepage: http://github.com/crapooze/welo
|
43
|
+
licenses: []
|
44
|
+
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options: []
|
47
|
+
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
segments:
|
56
|
+
- 0
|
57
|
+
version: "0"
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
segments:
|
64
|
+
- 0
|
65
|
+
version: "0"
|
66
|
+
requirements: []
|
67
|
+
|
68
|
+
rubyforge_project: welo
|
69
|
+
rubygems_version: 1.3.7
|
70
|
+
signing_key:
|
71
|
+
specification_version: 3
|
72
|
+
summary: A light resource model for Ruby
|
73
|
+
test_files: []
|
74
|
+
|