filemaker 0.0.1 → 0.0.2

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