welo 0.0.6
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.
- 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
|
+
|