filemaker 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +129 -17
  3. data/filemaker.gemspec +1 -0
  4. data/lib/filemaker.rb +47 -0
  5. data/lib/filemaker/api/query_commands/findquery.rb +20 -3
  6. data/lib/filemaker/configuration.rb +1 -1
  7. data/lib/filemaker/core_ext/hash.rb +19 -15
  8. data/lib/filemaker/error.rb +5 -0
  9. data/lib/filemaker/metadata/field.rb +21 -5
  10. data/lib/filemaker/model.rb +132 -0
  11. data/lib/filemaker/model/builder.rb +52 -0
  12. data/lib/filemaker/model/components.rb +25 -0
  13. data/lib/filemaker/model/criteria.rb +101 -0
  14. data/lib/filemaker/model/field.rb +38 -0
  15. data/lib/filemaker/model/fields.rb +80 -0
  16. data/lib/filemaker/model/findable.rb +35 -0
  17. data/lib/filemaker/model/optional.rb +69 -0
  18. data/lib/filemaker/model/pagination.rb +41 -0
  19. data/lib/filemaker/model/persistable.rb +96 -0
  20. data/lib/filemaker/model/relations.rb +72 -0
  21. data/lib/filemaker/model/relations/belongs_to.rb +30 -0
  22. data/lib/filemaker/model/relations/has_many.rb +79 -0
  23. data/lib/filemaker/model/relations/proxy.rb +35 -0
  24. data/lib/filemaker/model/selectable.rb +146 -0
  25. data/lib/filemaker/railtie.rb +17 -0
  26. data/lib/filemaker/record.rb +25 -0
  27. data/lib/filemaker/resultset.rb +12 -4
  28. data/lib/filemaker/server.rb +7 -5
  29. data/lib/filemaker/version.rb +1 -1
  30. data/spec/filemaker/api/query_commands/compound_find_spec.rb +13 -1
  31. data/spec/filemaker/configuration_spec.rb +23 -0
  32. data/spec/filemaker/layout_spec.rb +0 -1
  33. data/spec/filemaker/model/criteria_spec.rb +304 -0
  34. data/spec/filemaker/model/relations_spec.rb +85 -0
  35. data/spec/filemaker/model_spec.rb +73 -0
  36. data/spec/filemaker/record_spec.rb +12 -1
  37. data/spec/spec_helper.rb +1 -0
  38. data/spec/support/filemaker.yml +13 -0
  39. data/spec/support/models.rb +38 -0
  40. metadata +44 -2
@@ -0,0 +1,52 @@
1
+ module Filemaker
2
+ module Model
3
+ module Builder
4
+ module_function
5
+
6
+ # Given an array of resultset, build out the exact same number of model
7
+ # objects.
8
+ def collection(resultset, klass)
9
+ models = []
10
+
11
+ resultset.each do |record|
12
+ object = klass.new
13
+
14
+ object.instance_variable_set('@new_record', false)
15
+ object.instance_variable_set('@record_id', record.record_id)
16
+ object.instance_variable_set('@mod_id', record.mod_id)
17
+
18
+ record.keys.each do |fm_field_name|
19
+ # record.keys are all lowercase
20
+ field = klass.find_field_by_name(fm_field_name)
21
+ next unless field
22
+
23
+ object.public_send("#{field.name}=", record[fm_field_name])
24
+ end
25
+
26
+ models << object
27
+ end
28
+
29
+ models
30
+ end
31
+
32
+ def single(resultset, klass)
33
+ record = resultset.first
34
+ object = klass.new
35
+
36
+ object.instance_variable_set('@new_record', false)
37
+ object.instance_variable_set('@record_id', record.record_id)
38
+ object.instance_variable_set('@mod_id', record.mod_id)
39
+
40
+ record.keys.each do |fm_field_name|
41
+ # record.keys are all lowercase
42
+ field = klass.find_field_by_name(fm_field_name)
43
+ next unless field
44
+
45
+ object.public_send("#{field.name}=", record[fm_field_name])
46
+ end
47
+
48
+ object
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,25 @@
1
+ require 'filemaker/model/fields'
2
+ require 'filemaker/model/findable'
3
+ require 'filemaker/model/relations'
4
+ require 'filemaker/model/persistable'
5
+
6
+ module Filemaker
7
+ module Model
8
+ module Components
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ extend Findable
13
+ extend ActiveModel::Callbacks
14
+ end
15
+
16
+ include ActiveModel::Model
17
+ include ActiveModel::Serializers::JSON
18
+ include ActiveModel::Serializers::Xml
19
+ include ActiveModel::Validations::Callbacks
20
+ include Fields
21
+ include Relations
22
+ include Persistable
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,101 @@
1
+ require 'filemaker/model/selectable'
2
+ require 'filemaker/model/optional'
3
+ require 'filemaker/model/builder'
4
+ require 'filemaker/model/pagination'
5
+
6
+ module Filemaker
7
+ module Model
8
+ # Criteria encapsulates query arguments and options to represent a single
9
+ # query. It has convenient query DSL like +where+ and +in+ to represent both
10
+ # -find and -findquery FileMaker query. On top of that you can negate any
11
+ # query with the +not+ clause to omit selection.
12
+ class Criteria
13
+ include Enumerable
14
+ include Selectable
15
+ include Optional
16
+ include Pagination
17
+
18
+ # @return [Filemaker::Model] the class of the model
19
+ attr_reader :klass
20
+
21
+ # @return [Hash. Array] represents the query arguments
22
+ attr_reader :selector
23
+
24
+ # @return [Hash] options like skip, limit and order
25
+ attr_reader :options
26
+
27
+ # @return [Array] keep track of where clause and in clause to not mix them
28
+ attr_reader :chains
29
+
30
+ def initialize(klass)
31
+ @klass = klass
32
+ @options = {}
33
+ @chains = []
34
+ @_page = 1
35
+ end
36
+
37
+ def to_s
38
+ "#{selector}, #{options}"
39
+ end
40
+
41
+ def each
42
+ execute.each { |record| yield record } if block_given?
43
+ end
44
+
45
+ def first
46
+ limit(1).execute.first
47
+ end
48
+
49
+ def all
50
+ execute
51
+ end
52
+
53
+ def limit?
54
+ !options[:max].nil?
55
+ end
56
+
57
+ # The count this criteria is capable of returning
58
+ #
59
+ # @return [Integer] the count
60
+ def count
61
+ limit(0)
62
+ if chains.include?(:where)
63
+ klass.api.find(selector, options).count
64
+ elsif chains.include?(:in)
65
+ klass.api.query(selector, options).count
66
+ else
67
+ klass.api.findall(options).count
68
+ end
69
+ end
70
+
71
+ protected
72
+
73
+ def execute
74
+ resultset = []
75
+ paginated = chains.include?(:page)
76
+
77
+ if chains.include?(:where)
78
+ # Use -find
79
+ resultset = klass.api.find(selector, options)
80
+ elsif chains.include?(:in)
81
+ # Use -findquery
82
+ resultset = klass.api.query(selector, options)
83
+ else
84
+ # Use -findall
85
+ limit(1) unless limit?
86
+ resultset = klass.api.findall(options)
87
+ end
88
+
89
+ models = Filemaker::Model::Builder.collection(resultset, klass)
90
+
91
+ if defined?(Kaminari) && paginated
92
+ Kaminari.paginate_array(models, resultset.count)
93
+ .page(@_page)
94
+ .per(options[:max])
95
+ else
96
+ models
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,38 @@
1
+ module Filemaker
2
+ module Model
3
+ class Field
4
+ attr_reader :name, :type, :default_value, :fm_name
5
+
6
+ def initialize(name, type, options = {})
7
+ @name = name
8
+ @type = type
9
+ @default_value = coerce(options.fetch(:default) { nil })
10
+
11
+ # We need to downcase because Filemaker::Record is
12
+ # HashWithIndifferentAndCaseInsensitiveAccess
13
+ @fm_name = (options.fetch(:fm_name) { name }).to_s.downcase
14
+ end
15
+
16
+ # From FileMaker to Ruby
17
+ def coerce(value)
18
+ return nil if value.nil?
19
+
20
+ if @type == String
21
+ value.to_s
22
+ elsif @type == Integer
23
+ value.to_i
24
+ elsif @type == BigDecimal
25
+ BigDecimal.new(value.to_s)
26
+ elsif @type == Date
27
+ return value if value.is_a? Date
28
+ Date.parse(value.to_s)
29
+ elsif @type == DateTime
30
+ return value if value.is_a? DateTime
31
+ DateTime.parse(value.to_s)
32
+ else
33
+ value
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,80 @@
1
+ require 'filemaker/model/field'
2
+
3
+ module Filemaker
4
+ module Model
5
+ module Fields
6
+ extend ActiveSupport::Concern
7
+
8
+ TYPE_MAPPINGS = {
9
+ string: String,
10
+ date: Date,
11
+ datetime: DateTime,
12
+ money: BigDecimal,
13
+ integer: Integer,
14
+ number: BigDecimal
15
+ }
16
+
17
+ included do
18
+ class_attribute :fields, :identity
19
+ self.fields = {}
20
+ end
21
+
22
+ def apply_defaults
23
+ attribute_names.each do |name|
24
+ field = fields[name]
25
+ attributes[name] = field.default_value
26
+ end
27
+ end
28
+
29
+ def attribute_names
30
+ self.class.attribute_names
31
+ end
32
+
33
+ def fm_names
34
+ fields.values.map(&:fm_name)
35
+ end
36
+
37
+ module ClassMethods
38
+ def attribute_names
39
+ fields.keys
40
+ end
41
+
42
+ %w(string date datetime money integer number).each do |type|
43
+ define_method(type) do |*args|
44
+ # TODO: It will be good if we can accept lambda also
45
+ options = args.last.is_a?(Hash) ? args.pop : {}
46
+ field_names = args
47
+
48
+ field_names.each do |name|
49
+ add_field(name, TYPE_MAPPINGS[type.to_sym], options)
50
+ create_accessors(name)
51
+ end
52
+ end
53
+ end
54
+
55
+ def add_field(name, type, options)
56
+ fields[name] = Filemaker::Model::Field.new(name, type, options)
57
+ self.identity = fields[name] if options[:identity]
58
+ end
59
+
60
+ def create_accessors(name)
61
+ define_method(name) { attributes[name] }
62
+ define_method("#{name}=") do |value|
63
+ attributes[name] = fields[name].coerce(value)
64
+ end
65
+ define_method("#{name}?") do
66
+ attributes[name] == true || attributes[name].present?
67
+ end
68
+ end
69
+
70
+ # Find FileMaker's real name given either the attribute name or the real
71
+ # FileMaker name.
72
+ def find_field_by_name(name)
73
+ fields.values.find do |f|
74
+ f.name == name.to_sym || f.fm_name == name.to_s
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,35 @@
1
+ module Filemaker
2
+ module Model
3
+ module Findable
4
+ delegate \
5
+ :limit,
6
+ :skip,
7
+ :order,
8
+ :find,
9
+ :first,
10
+ :recid,
11
+ :in,
12
+ :not_in,
13
+ :or,
14
+ :eq,
15
+ :cn,
16
+ :bw,
17
+ :ew,
18
+ :gt,
19
+ :gte,
20
+ :lt,
21
+ :lte,
22
+ :neq,
23
+ :equals,
24
+ :contains,
25
+ :begins_with,
26
+ :ends_with,
27
+ :not,
28
+ :where, to: :criteria
29
+
30
+ def criteria
31
+ Criteria.new(self)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,69 @@
1
+ module Filemaker
2
+ module Model
3
+ module Optional
4
+ # Number of records to skip. For pagination.
5
+ #
6
+ # @param [Integer] value The number to skip.
7
+ #
8
+ # @return [Filemaker::Model::Criteria]
9
+ def skip(value)
10
+ return self if value.nil?
11
+ options[:skip] = value.to_i
12
+ yield options if block_given?
13
+ self
14
+ end
15
+
16
+ # Limit the number of records returned.
17
+ #
18
+ # @param [Integer] value The number of records to return.
19
+ #
20
+ # @return [Filemaker::Model::Criteria]
21
+ def limit(value)
22
+ return self if value.nil?
23
+ options[:max] = value.to_i
24
+ yield options if block_given?
25
+ self
26
+ end
27
+
28
+ # Order the records. Model field name will be converted to real FileMaker
29
+ # field name.
30
+ #
31
+ # @example Sort is position aware!
32
+ # criteria.order('name desc, email')
33
+ #
34
+ # @param [String] value The sorting string
35
+ #
36
+ # @return [Filemaker::Model::Criteria]
37
+ def order(value)
38
+ return self if value.nil?
39
+ sortfield = []
40
+ sortorder = []
41
+ sort_spec = value.split(',').map(&:strip)
42
+
43
+ sort_spec.each do |spec|
44
+ fieldname, direction = spec.split(' ')
45
+ direction = 'asc' unless direction
46
+
47
+ field = klass.find_field_by_name(fieldname)
48
+
49
+ next unless field
50
+
51
+ direction = 'ascend' if direction.downcase == 'asc'
52
+ direction = 'descend' if direction.downcase == 'desc'
53
+
54
+ sortfield << field.fm_name
55
+ sortorder << direction
56
+ end
57
+
58
+ unless sortfield.empty?
59
+ options[:sortfield] = sortfield
60
+ options[:sortorder] = sortorder
61
+ end
62
+
63
+ yield options if block_given?
64
+
65
+ self
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,41 @@
1
+ module Filemaker
2
+ module Model
3
+ module Pagination
4
+ # Calling `page` will trigger pagination.
5
+ def page(value)
6
+ chains << :page
7
+ @_page = value.to_i
8
+ update_skip
9
+ end
10
+
11
+ def per(value)
12
+ limit(value)
13
+ update_skip
14
+ end
15
+
16
+ # A simple getter to retrieve the current page value. If no one set it up
17
+ # through the `page(4)` way, then at least it defaults to 1.
18
+ def __page
19
+ @_page || 1
20
+ end
21
+
22
+ # A simple getter to retrieve the limit value. It will default to
23
+ # Model.per_page
24
+ #
25
+ # Will have stacklevel too deep if we have `per(nil)`. Somehow, the
26
+ # `per_page` must be set either at the `Model.per_page`,
27
+ # `Kaminari.config.default_per_page`, or right here where I just throw a
28
+ # 25 value at it.
29
+ def __per
30
+ per(klass.per_page || 25) unless limit?
31
+ options[:max]
32
+ end
33
+
34
+ def update_skip
35
+ skip = (__page - 1) * __per
36
+ skip(skip) unless skip.zero?
37
+ self
38
+ end
39
+ end
40
+ end
41
+ end